<file_sep>import {User} from './User';

export class Conversation {
	id: number;
	chatName: string;
	hidden: boolean;
	users: User[];
}
<file_sep>create table conversation
(
    id         bigserial not null
        constraint conversation_pkey
            primary key,
    chat_name  varchar,
    creator_id bigint
);

create unique index conversation_id_uindex
    on conversation (id);

create table "user"
(
    id    bigserial not null
        constraint user_pkey
            primary key,
    login varchar   not null,
    hash  varchar   not null
);

create unique index user_id_uindex
    on "user" (id);

create unique index user_login_uindex
    on "user" (login);

create table message
(
    id              bigserial                not null
        constraint message_pkey
            primary key,
    conversation_id bigint                   not null
        constraint message_conversation_id_fk
            references conversation,
    sent            timestamp with time zone not null,
    text            varchar,
    sender_id       bigint                   not null
        constraint message_user_id_fk
            references "user"
);

create unique index message_id_uindex
    on message (id);

create table forwarded_message
(
    parent_message_id    bigint not null
        constraint forwarded_message_message_id_fk
            references message,
    forwarded_message_id bigint not null
        constraint forwarded_message_message_id_fk_2
            references message
);

create table token
(
    id      bigserial not null
        constraint token_pk
            primary key,
    user_id bigint    not null
        constraint token_user_id_fk
            references "user",
    token   varchar   not null
);

create table user_info
(
    id                 bigserial not null
        constraint user_main_info_2_pk
            primary key,
    user_id            bigint    not null
        constraint user_main_info_2_user_id_fk
            references "user",
    first_name         varchar   not null,
    last_name          varchar   not null,
    gender             boolean,
    birth_date         date,
    marital_status     varchar,
    country            varchar,
    city               varchar,
    location           varchar,
    phone_number       varchar,
    mail               varchar,
    place_of_education varchar,
    place_of_work      varchar,
    about              varchar
);

create unique index user_main_info_2_id_uindex
    on user_info (id);

create table user_online
(
    id      bigserial                not null
        constraint user_online_pk
            primary key,
    user_id bigint                   not null
        constraint user_online_user_id_fk
            references "user",
    seen    timestamp with time zone not null
);

create unique index user_online_id_uindex
    on user_online (id);

create table avatar
(
    id       bigserial not null
        constraint avatar_pk
            primary key,
    user_id  bigint    not null
        constraint avatar_user_id_fk
            references "user",
    path     varchar   not null,
    uploaded date      not null
);

create table image
(
    id         bigserial not null
        constraint image_pk
            primary key,
    user_id    bigint    not null
        constraint image_user_id_fk
            references "user",
    message_id bigint    not null
        constraint image_message_id_fk
            references message,
    path       varchar   not null,
    uploaded   date      not null
);

create unique index image_id_uindex
    on image (id);

create unique index image_path_uindex
    on image (path);

create unique index document_id_uindex
    on image (id);

create unique index document_path_uindex
    on image (path);

create table document
(
    id         bigserial not null
        constraint document_pkey
            primary key,
    user_id    bigint    not null
        constraint document_user_id_fk
            references "user",
    message_id bigint    not null
        constraint document_message_id_fk
            references message,
    path       varchar   not null,
    uploaded   date      not null
);

create table user_conversation
(
    id              bigserial                not null
        constraint user_conversation_pk
            primary key,
    user_id         bigint                   not null
        constraint user_conversation_user_id_fk
            references "user",
    conversation_id bigint                   not null
        constraint user_conversation_conversation_id_fk
            references conversation,
    hidden          boolean default false,
    last_read       timestamp with time zone not null,
    kicked          boolean default false
);

create unique index user_conversation_id_uindex
    on user_conversation (id);

INSERT INTO public."user" (id, login, hash)
VALUES (0, 'system', '9ORQk/lZ9xIqQRHYpKUnFIPoAz31kj0xCz47u8FHE7k=$/5l1c6S8EZGjrFaJoMXpI0fGSHisgBj3lv9Ldiejou0=');

INSERT INTO public.user_info (id, user_id, first_name, last_name, gender, birth_date, marital_status, country, city,
                              location, phone_number, mail, place_of_education, place_of_work, about)
VALUES (0, 0, 'system', '', null, null, null, null, null, null, null, null, null, null, null);<file_sep>package com.gmail.ivanjermakov1.messenger.test.unit;

import com.gmail.ivanjermakov1.messenger.dto.NewChatDto;
import com.gmail.ivanjermakov1.messenger.dto.TestingUser;
import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.RegistrationException;
import com.gmail.ivanjermakov1.messenger.service.ChatService;
import com.gmail.ivanjermakov1.messenger.service.TestingService;
import com.gmail.ivanjermakov1.messenger.service.UserService;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collections;

@Ignore
@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class ChatUnitTest {

	@Autowired
	private UserService userService;

	@Autowired
	private ChatService chatService;

	@Autowired
	private TestingService testingService;
	private TestingUser user1;
	private TestingUser user2;
	private Conversation chat;

	@Before
	public void before() throws RegistrationException, AuthenticationException {
		user1 = testingService.registerUser("Jack");
		user2 = testingService.registerUser("Ron");

		chat = chatService.create(
				user1.user,
				new NewChatDto(
						"chat",
						Collections.singletonList(user2.user.id)
				)
		);
	}

}
<file_sep>import {Component, ElementRef, HostListener, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {Title} from '@angular/platform-browser';


import * as moment from 'moment';
import {Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';
import {User} from '../../../dto/User';
import {Preview} from '../../../dto/Preview';
import {Message} from '../../../dto/Message';
import {MessageAttachments} from '../../../dto/MessageAttachments';
import {ImageCompressionMode} from '../../../dto/enum/ImageCompressionMode';
import {ImageService} from '../../../service/image.service';
import {NewImage} from '../../../dto/NewImage';
import {AppComponent} from '../../../app.component';
import {MeProvider} from '../../../provider/me-provider';
import {MessageService} from '../../../service/message.service';
import {TokenProvider} from '../../../provider/token-provider';
import {PreviewService} from '../../../service/preview.service';
import {AuthService} from '../../../service/auth.service';
import {SearchService} from '../../../service/search.service';
import {CookieService} from '../../../service/cookie.service';
import {MessagingService} from '../../../service/messaging.service';
import {ConversationService} from '../../../service/conversation.service';
import {SoundNotificationService} from '../../../service/sound-notification.service';
import {UserInfoService} from '../../../service/user-info.service';
import {BackgroundUnreadService} from '../../../service/background-unread.service';
import {AvatarService} from '../../../service/avatar.service';
import {APP_TITLE, FILE_URL, MESSAGES_AMOUNT, MINUTES_AS_ONLINE_LIMIT, PREVIEWS_AMOUNT} from '../../../../../globals';
import {NewMessage} from '../../../dto/NewMessage';
import {DateService} from '../../../service/date.service';
import {NewMessageAction} from '../../../dto/action/NewMessageAction';
import {ConversationReadAction} from '../../../dto/action/ConversationReadAction';
import {MessageEditAction} from '../../../dto/action/MessageEditAction';
import {Pageable} from '../../../dto/Pageable';
import {NewDocument} from '../../../dto/NewDocument';
import {MessageImage} from '../../../dto/local/MessageImage';
import {ChatService} from '../../../service/chat.service';
import {NewChat} from '../../../dto/NewChat';
import {PreviewType} from '../../../dto/enum/PreviewType';
import {ConfirmService} from '../../../service/confirm.service';

@Component({
	selector: 'app-messaging',
	templateUrl: './messaging.component.html',
	styleUrls: [
		'./messaging.component.scss',
		'./style/messaging.component.header-left.scss',
		'./style/messaging.component.header-right.scss',
		'./style/messaging.component.search.scss',
		'./style/messaging.component.select-message.scss',
		'./style/messaging.component.content-left.scss',
		'./style/messaging.component.conversation.scss'
	]
})
export class MessagingComponent implements OnInit {

	readonly ImageCompressionMode: typeof ImageCompressionMode = ImageCompressionMode;
	readonly ImageService: typeof ImageService = ImageService;
	readonly FILE_URL = FILE_URL;
	readonly PreviewType: typeof PreviewType = PreviewType;

	token: string;
	isPolling = false;

	me: User;

	previews: Preview[];
	messages: Message[];
	routeConversationId: number;
	currentPreview: Preview;

	messageText = '';

	searchText = '';
	searchTextChanged: Subject<string> = new Subject();

	searchUsers: User[] = [];
	searchPreviews: Preview[] = [];

	selectedMessages: Message[] = [];

	editingMessage: Message;
	currentMessageAttachments: MessageAttachments = new MessageAttachments();

	currentProfile = {
		userInfo: null,
		user: null
	};

	isLeftView = true;
	isSelectForwardTo = false;

	attachedImages: NewImage[] = [];
	attachedDocuments: NewDocument[] = [];

	messageImage: MessageImage = null;

	showChatInfo = false;

	@ViewChild('sendMessageText') sendMessageText: ElementRef;
	@ViewChild('previewSearch') previewSearch: ElementRef;
	@ViewChild('messageWrapper') messageWrapper: ElementRef;

	@HostListener('document:keydown', ['$event'])
	handleKeyboardEvent(event: KeyboardEvent) {
		if (event.key === 'Escape') {
			if (this.messageImage) {
				this.messageImage = null;
				return;
			}

			if (this.currentProfile.user) {
				this.currentProfile.user = null;
				return;
			}

			if (this.showChatInfo) {
				this.showChatInfo = false;
				return;
			}

			if (this.editingMessage) {
				this.cancelEditing();
				return;
			}

			if (!this.isSelectForwardTo && this.currentMessageAttachments.forwarded.length !== 0) {
				this.currentMessageAttachments.forwarded = [];
				return;
			}

			if (this.isSelectForwardTo) {
				this.isSelectForwardTo = false;
				this.currentMessageAttachments.forwarded = [];
				return;
			}

			if (this.searchText !== '') {
				this.searchText = '';
				return;
			}

			if (this.selectedMessages.length !== 0) {
				this.deselectMessages();
				return;
			}

			if (this.currentPreview) {
				this.closeConversation();
			}
		}
	}

	@HostListener('window:focus', ['$event'])
	onFocus(event: any): void {
		this.backgroundUnreadService.resetUnreadCount();
		this.titleService.setTitle(APP_TITLE);
	}

	constructor(private app: AppComponent,
	            private route: ActivatedRoute,
	            private router: Router,
	            private titleService: Title,
	            private previewService: PreviewService,
	            private tokenProvider: TokenProvider,
	            private meProvider: MeProvider,
	            private messageService: MessageService,
	            private authService: AuthService,
	            private searchService: SearchService,
	            private cookieService: CookieService,
	            private messagingService: MessagingService,
	            private conversationService: ConversationService,
	            private chatService: ChatService,
	            private soundNotificationService: SoundNotificationService,
	            private userInfoService: UserInfoService,
	            private backgroundUnreadService: BackgroundUnreadService,
	            private avatarService: AvatarService,
	            private imageService: ImageService,
	            private confirmService: ConfirmService
	) {
	}

	ngOnInit() {
		this.searchTextChanged
			.pipe(debounceTime(200), distinctUntilChanged())
			.subscribe(searchText => {
				this.searchForConversationsOrUsers(searchText);
			});

		this.app.onLoad(() => {
			this.route.queryParams.subscribe(params => {
				if (!this.isPolling) {
					this.meProvider.oMe.subscribe(me => {
						return this.me = me;
					});

					this.tokenProvider.oToken.subscribe(token => {
						this.token = token;

						this.startListening();
					});
				}

				this.routeConversationId = params['id'];
				if (this.routeConversationId) {
					this.isLeftView = false;
					this.loadCurrentConversation();
				} else {
					this.isLeftView = true;
					this.currentPreview = null;
					this.messages = [];
				}

				this.updatePreviews();
			});
		});
	}

	updatePreviews() {
		this.previewService.all(this.token, new Pageable(0, PREVIEWS_AMOUNT)).subscribe(previews => {
			this.previews = previews.filter(p => p.lastMessage && !p.conversation.hidden);
		});
	}

	updateMessages() {
		this.messageService.get(this.token, this.routeConversationId, new Pageable(0, MESSAGES_AMOUNT)).subscribe(messages => {
			this.messages = messages;
		});
	}

	previewsOrSearchPreviews(): Preview[] {
		if (this.searchText === '') {
			return this.previews;
		} else {
			return this.searchPreviews;
		}
	}

	openConversation(conversationId: number) {
		if (this.currentPreview && this.currentPreview.conversation.id === conversationId) return;

		this.router.navigate(['/im'], {queryParams: {id: conversationId}});
		this.messages = null;
		this.isSelectForwardTo = false;
		this.sendMessageTextFocus();
	}

	closeConversation() {
		this.router.navigate(['/im']);
	}

	createConversation(user: User) {
		this.conversationService.create(this.token, user.login).subscribe(conversation => {
			this.openConversation(conversation.id);
		});
	}

	createChat(chatName: string) {
		let newChat = new NewChat();
		newChat.name = chatName;
		this.chatService.create(this.token, newChat).subscribe(chat => {
			this.openConversation(chat.id);
		});
	}

	searchForConversationsOrUsers(searchText: string) {
		if (searchText === '') return;

		if (searchText[0] === '@') {
			this.searchService.searchUsers(this.token, searchText)
				.subscribe(users => this.searchUsers = users);
		} else {
			this.searchService.searchConversations(this.token, searchText)
				.subscribe(conversations => this.searchPreviews = conversations);
		}
	}

	sendMessage() {
		const message = new NewMessage();
		message.senderId = this.me.id;
		message.conversationId = this.currentPreview.conversation.id;
		message.text = this.messageText;
		message.forwarded = this.currentMessageAttachments.forwarded;
		message.images = this.attachedImages;
		message.documents = this.attachedDocuments;

		if ((message.forwarded.length === 0 && message.images.length === 0 && message.documents.length === 0) && message.text.trim().length === 0) return;

		this.messageText = '';
		this.currentMessageAttachments = new MessageAttachments();

		// TODO: use temp unique id to identify temp message instead of unshifting last
		const tempViewMessage = new Message();
		tempViewMessage.sender = this.me;
		tempViewMessage.forwarded = message.forwarded;
		tempViewMessage.text = message.text;

		this.messages.unshift(tempViewMessage);

		this.messagingService.sendMessage(this.token, message).subscribe(m => {
			this.updatePreviews();
			this.messages = this.messages.filter(mes => mes.id);
			this.messages.unshift(m);
			this.attachedImages = [];
			this.attachedDocuments = [];
		});
	}

	selectMessage(message: Message, event?) {
		if (event.target.className === 'avatar') return;

		if (!message.selected) {
			message.selected = true;
			this.selectedMessages.push(message);
		} else {
			message.selected = false;
			this.selectedMessages = this.selectedMessages.filter(m => m.id !== message.id);
		}
	}

	deselectMessages() {
		this.selectedMessages.forEach(m => m.selected = false);
		this.selectedMessages = [];
	}

	deleteSelectedMessages() {
		this.messageService.delete(this.token, this.selectedMessages).subscribe(
			() => {
				this.updatePreviews();
				this.updateMessages();
			},
			error => {
				this.updatePreviews();
				this.updateMessages();
			}
		);
		this.deselectMessages();
	}

	editSelectedMessage() {
		this.editingMessage = this.selectedMessages[0];
		this.messageText = this.editingMessage.text;
		this.sendMessageTextFocus();
	}

	editMessage() {
		this.editingMessage.text = this.messageText;

		this.messagingService.editMessage(this.token, this.editingMessage).subscribe(m => {
			this.cancelEditing();
			this.messageImage = null;
		});
	}

	attachSelectedMessagesAsForwardedAttachment() {
		this.currentMessageAttachments.forwarded = this.selectedMessages;
		this.deselectMessages();
	}

	forwardSelectedMessages() {
		this.attachSelectedMessagesAsForwardedAttachment();
		this.isSelectForwardTo = true;
		this.isLeftView = true;
	}

	removeForwardedAttachment() {
		this.currentMessageAttachments.forwarded = [];
	}

	cancelEditing() {
		this.deselectMessages();
		this.editingMessage = null;
		this.messageText = '';
	}

	incrementBackgroundUnread() {
		if (document.visibilityState === 'hidden') {
			this.backgroundUnreadService.incrementUnreadCount();
			this.backgroundUnreadService.oUnreadCount.subscribe(c => {
				this.titleService.setTitle(`${APP_TITLE} ${c} new message${c === 1 ? '' : 's'}`);
			});
		}
	}

	lastSeenView(lastSeen: Date): string {
		return DateService.lastSeenView(lastSeen);
	}

	isOnline(time: Date): boolean {
		return time && moment().diff(time, 'minutes') < MINUTES_AS_ONLINE_LIMIT;
	}

	loadMoreMessages() {
		if (this.messages.length % MESSAGES_AMOUNT !== 0) return;
		const pageable = new Pageable(Math.floor(this.messages.length / MESSAGES_AMOUNT), MESSAGES_AMOUNT);
		this.messageService.get(this.token, this.currentPreview.conversation.id, pageable).subscribe(messages => {
			this.messages = this.messages.concat(messages);
		});
	}

	loadMorePreviews() {
		if (this.previews.length % PREVIEWS_AMOUNT !== 0) return;
		const pageable = new Pageable(Math.floor(this.previews.length / PREVIEWS_AMOUNT), PREVIEWS_AMOUNT);
		this.previewService.all(this.token, pageable).subscribe(previews => {
			this.previews = this.previews.concat(previews);
		});
	}

	scrollToBottom() {
		if (this.messageWrapper) this.messageWrapper.nativeElement.scrollTop = this.messageWrapper.nativeElement.scrollHeight;
	}

	openPreviewInfo() {
		switch (+PreviewType[this.currentPreview.type]) {
			case PreviewType.CONVERSATION:
				this.openProfile(this.currentPreview.with);
				break;
			case PreviewType.CHAT:
				this.showChatInfo = true;
				break;
		}
	}

	openProfile(user: User) {
		this.userInfoService.get(this.token, user.id).subscribe(userInfo => {
			this.currentProfile.user = user;
			this.currentProfile.userInfo = userInfo;
		});
	}

	editProfile(userInfo) {
		this.userInfoService.edit(this.token, userInfo).subscribe(info => {
			this.uploadAvatar(userInfo.newAvatar);

			this.currentProfile.userInfo = info;
		});
	}

	uploadAvatar(avatar: File) {
		if (!avatar) {
			return;
		}

		this.avatarService.upload(this.token, avatar).subscribe(avatarResponse => {
			this.currentProfile.user.avatar = avatarResponse;
		});
	}

	removeImageAttachment(image: NewImage) {
		this.attachedImages = this.attachedImages.filter(i => i.path === image.path);
	}

	removeDocumentAttachment(document: NewDocument) {
		this.attachedDocuments = this.attachedDocuments.filter(d => d.path === document.path);
	}

	sendMessageTextFocus() {
		setTimeout(() => {
			if (this.sendMessageText) this.sendMessageText.nativeElement.focus();
		}, 0);
	}

	previewSearchFocus() {
		setTimeout(() => {
			if (this.previewSearch) {
				this.previewSearch.nativeElement.focus();
			}
		}, 0);
	}

	deleteImageFromMessage() {
		this.imageService.delete(this.token, this.messageImage.image.id).subscribe(() => {
			this.editingMessage = this.messageImage.message;
			this.editMessage();
		});
	}

	addToChat(user: User) {
		if (this.currentPreview.type.toString() !== PreviewType[PreviewType.CHAT]) return;

		this.chatService.addMember(this.token, this.currentPreview.conversation.id, user.id).subscribe(() => {
			this.loadOrUpdatePreview(this.currentPreview.conversation.id);
		});
	}

	kickMember(user: User) {
		this.confirmService.confirm(`${user.firstName} will be kicked from chat`);
		this.chatService.kickMember(this.token, this.currentPreview.conversation.id, user.id).subscribe(() => {
			this.loadOrUpdatePreview(this.currentPreview.conversation.id);
		});
	}

	private loadCurrentConversation() {
		this.previewService.get(this.token, this.routeConversationId).subscribe(preview => {
			this.currentPreview = preview;
			this.scrollToBottom();
		}, error => this.closeConversation());
		this.messageService.get(this.token, this.routeConversationId, new Pageable(0, MESSAGES_AMOUNT)).subscribe(messages => {
			this.searchText = '';
			this.messages = messages;
		});
	}

	private loadOrUpdatePreview(conversationId: number) {
		this.previewService.get(this.token, conversationId).subscribe(preview => {
			const existingPreview = this.previews.find(p => p.conversation.id === preview.conversation.id);
			if (existingPreview) {
				Object.assign(existingPreview, preview);
			} else {
				this.previews.push(preview);
			}
			if (preview.conversation.id == this.routeConversationId) {
				this.currentPreview = preview;
			}
		});
	}

	private startListening() {
		this.isPolling = true;
		this.messagingService.getEvents(this.token).subscribe(action => {
			console.debug(action);
			setTimeout(() => {
				switch (action.type) {
					case 'NEW_MESSAGE':
						this.processNewMessage(action);
						break;
					case 'CONVERSATION_READ':
						this.processRead(action);
						break;
					case 'MESSAGE_EDIT':
						this.processEdit(action);
						break;
				}
				// TODO: investigate magic
			}, 100);
		});
	}

	private processNewMessage(action: NewMessageAction) {
		this.loadOrUpdatePreview(action.message.conversation.id);

		if (action.message.sender.id === this.me.id) return;

		this.incrementBackgroundUnread();
		this.soundNotificationService.notify();

		if (this.currentPreview && action.message.conversation.id === this.currentPreview.conversation.id) {
			this.messages.unshift(action.message);
		}
	}

	private processRead(action: ConversationReadAction) {
		this.loadOrUpdatePreview(action.conversation.id);

		if (action.reader.id === this.me.id) return;
		if (this.messages && this.currentPreview && action.conversation.id === this.currentPreview.conversation.id) {
			this.messages.forEach(m => m.read = true);
		}
	}

	private processEdit(action: MessageEditAction) {
		this.loadOrUpdatePreview(action.message.conversation.id);

		if (this.currentPreview && action.message.conversation.id === this.currentPreview.conversation.id) {
			const updatedMessage = this.messages.find(_m => _m.id === action.message.id);
			Object.assign(updatedMessage, action.message);
		}
	}
}
<file_sep>package com.gmail.ivanjermakov1.messenger.repository;

import com.gmail.ivanjermakov1.messenger.entity.Image;
import org.springframework.data.repository.CrudRepository;

public interface ImageRepository extends CrudRepository<Image, Long> {


}
<file_sep>import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs';

@Injectable({
	providedIn: 'root'
})
export class TokenProvider {

	private token = new BehaviorSubject<string>('');
	oToken = this.token.asObservable();

	constructor() {
	}

	setToken(token: string) {
		this.token.next(token);
	}

}
<file_sep>export enum MaritalStatus {
	SINGLE,
	SEEING,
	MARRIED
}
<file_sep>package com.gmail.ivanjermakov1.messenger.validator;

import com.github.ivanjermakov.jtrue.core.Validatable;
import com.github.ivanjermakov.jtrue.core.Validator;
import com.github.ivanjermakov.jtrue.predicate.NotEmptyCollection;
import com.github.ivanjermakov.jtrue.predicate.NotNull;
import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import com.gmail.ivanjermakov1.messenger.entity.Document;
import com.gmail.ivanjermakov1.messenger.entity.Image;
import com.gmail.ivanjermakov1.messenger.entity.Message;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.exception.InvalidEntityException;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.function.Function;

@Component
public class MessageValidator implements Validatable<Message> {

	private Validator<Message> attachmentValidator = new Validator<Message>()
			.map(m -> m.forwarded).rule(new NotNull<List<Message>>().and(new NotEmptyCollection<>()).negate())
			.map(m -> m.images).rule(new NotNull<List<Image>>().and(new NotEmptyCollection<>()).negate())
			.map(m -> m.documents).rule(new NotNull<List<Document>>().and(new NotEmptyCollection<>()).negate());

	private Validator<Message> validator = new Validator<Message>()
			.map(m -> m.sender).use(new Validator<User>()
					.rule(new NotNull<>(), "user cannot be null")
					.map(u -> u.id).rule(new NotNull<>(), "user id cannot be null")
			)
			.map(m -> m.conversation).use(new Validator<Conversation>()
					.rule(new NotNull<>(), "conversation cannot be null")
					.map(c -> c.id).rule(new NotNull<>(), "conversation id cannot be null")
			)
			.rule(m -> !m.text.isEmpty() || !attachmentValidator.validate(m), "message without text must contain attachments")
			.throwing((Function<String, Throwable>) InvalidEntityException::new);

	@Override
	public boolean validate(Message target) {
		return validator.validate(target);
	}

	@Override
	public void throwInvalid(Message target) throws InvalidEntityException {
		try {
			validator.throwInvalid(target);
		} catch (InvalidEntityException e) {
			throw e;
		} catch (Throwable ignored) {
		}
	}
}
<file_sep>package com.gmail.ivanjermakov1.messenger.test.integration;

import com.gmail.ivanjermakov1.messenger.controller.AuthenticationController;
import com.gmail.ivanjermakov1.messenger.controller.RegistrationController;
import com.gmail.ivanjermakov1.messenger.dto.RegisterUserDto;
import com.gmail.ivanjermakov1.messenger.dto.UserDto;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.RegistrationException;
import com.gmail.ivanjermakov1.messenger.service.UserService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class AuthenticationTest {

	@Autowired
	private AuthenticationController authenticationController;

	@Autowired
	private UserService userService;

	@Autowired
	private RegistrationController registrationController;

	@Test
	public void shouldAuthenticate() throws RegistrationException, AuthenticationException {
		registrationController.register(
				new RegisterUserDto("Jack", "Johnson", "jackj", "<PASSWORD>")
		);

		String token = authenticationController.authenticate("jackj", "<PASSWORD>");

		Assert.assertNotNull(token);

		UserDto user = authenticationController.validate(
				userService.authenticate(token),
				token
		);

		Assert.assertNotNull(user);
		Assert.assertEquals("jackj", user.login);
	}

	@Test(expected = AuthenticationException.class)
	public void shouldThrowException_WithWrongPassword() throws RegistrationException, AuthenticationException {
		registrationController.register(
				new RegisterUserDto("Jack", "Johnson", "jackj", "password")
		);

		authenticationController.authenticate("jackj", "not_password");
	}

	@Test(expected = AuthenticationException.class)
	public void shouldLogout() throws RegistrationException, AuthenticationException {
		registrationController.register(
				new RegisterUserDto("Jack", "Johnson", "jackj", "<PASSWORD>")
		);

		String token = authenticationController.authenticate("jackj", "<PASSWORD>");

		Assert.assertNotNull(token);

		User user = userService.authenticate(token);

		UserDto userDto = authenticationController.validate(user, token);

		Assert.assertNotNull(userDto);
		Assert.assertEquals("jackj", userDto.login);

		authenticationController.logout(user);

		authenticationController.validate(user, token);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.mapper;

import com.gmail.ivanjermakov1.messenger.dto.AvatarDto;
import com.gmail.ivanjermakov1.messenger.entity.Avatar;
import com.gmail.ivanjermakov1.messenger.util.Mappers;
import org.springframework.stereotype.Component;

@Component
public class AvatarMapper implements Mapper<Avatar, AvatarDto> {

	@Override
	public AvatarDto map(Avatar avatar) {
		return Mappers.map(avatar, AvatarDto.class);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.test.unit;

import com.gmail.ivanjermakov1.messenger.dto.TestingUser;
import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import com.gmail.ivanjermakov1.messenger.exception.RegistrationException;
import com.gmail.ivanjermakov1.messenger.repository.UserConversationRepository;
import com.gmail.ivanjermakov1.messenger.service.ConversationService;
import com.gmail.ivanjermakov1.messenger.service.MessageService;
import com.gmail.ivanjermakov1.messenger.service.TestingService;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.when;

@Ignore
@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class MessageUnitTest {

	@InjectMocks
	private MessageService messageService;

	@Mock
	private UserConversationRepository userConversationRepository;

	@Autowired
	private ConversationService conversationService;

	@Autowired
	private TestingService testingService;

	@Test(expected = NoSuchEntityException.class)
	public void shouldThrowException_WhenGetWithInvalidConversationId() throws RegistrationException, AuthenticationException {
		TestingUser user1 = testingService.registerUser("Jack");
		TestingUser user2 = testingService.registerUser("Ron");
		Conversation conversation = conversationService.create(user1.user, user2.user);

		when(userConversationRepository.findByUserAndConversation(any(), any()))
				.thenReturn(Optional.empty());

		messageService.get(user1.user.id, conversation.id, Pageable.unpaged());
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.dto;

import java.util.List;

public class ConversationDto {

	public Long id;
	public String chatName;
	public Boolean hidden;
	public List<UserDto> users;

	public ConversationDto() {
	}

	public ConversationDto(Long id, String chatName, Boolean hidden, List<UserDto> users) {
		this.id = id;
		this.chatName = chatName;
		this.hidden = hidden;
		this.users = users;
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.controller;

import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.InvalidEntityException;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;

public interface PasswordRecoveryController {

	/**
	 * Request password recovery using specified in user info mail
	 *
	 * @param login user login
	 * @throws AuthenticationException on invalid @param login
	 * @throws NoSuchEntityException   if mail is not specified
	 */
	//	TODO: eager mail verification
	void requestMail(String login) throws AuthenticationException, NoSuchEntityException, InvalidEntityException;

	/**
	 * Request password recovery using specified in user info phone number
	 *
	 * @param login user login
	 * @throws AuthenticationException on invalid @param login
	 * @throws NoSuchEntityException   if phone number is not specified
	 */
	//TODO: eager mobile verification
	void requestPhone(String login) throws AuthenticationException, NoSuchEntityException, InvalidEntityException;

	/**
	 * Sets new password using email token received using `requestMail()` method
	 *
	 * @param token       received mail token
	 * @param newPassword <PASSWORD>
	 * @throws AuthenticationException on invalid @param token
	 * @throws InvalidEntityException  if password is invalid
	 */
	void changePasswordMail(String token, String newPassword) throws AuthenticationException, InvalidEntityException;

	/**
	 * Sets new password using code received using `requestPhone()` method
	 *
	 * @param code        received phone code
	 * @param newPassword <PASSWORD>
	 * @throws AuthenticationException on invalid @param code
	 * @throws InvalidEntityException  if password is invalid
	 */
	void changePasswordPhone(String code, String newPassword) throws AuthenticationException, InvalidEntityException;

}
<file_sep>package com.gmail.ivanjermakov1.messenger.dto.action;

import com.gmail.ivanjermakov1.messenger.dto.MessageDto;

public class MessageEditAction extends Action {

	public MessageDto message;

	public MessageEditAction() {
		type = Type.MESSAGE_EDIT;
	}

	public MessageEditAction(MessageDto message) {
		this();
		this.message = message;
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.entity;

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.time.LocalDate;

/**
 * Entity representing file attached to a certain message
 */
@Entity
@Access(AccessType.FIELD)
@Table(name = "document")
public class Document {

	/**
	 * Document id
	 */
	@Id
	@Column(name = "id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Long id;

	/**
	 * Owner user of a document
	 */
	@ManyToOne
	@JoinColumn(name = "user_id")
	public User user;

	/**
	 * Static resource path to a document
	 */
	@Column(name = "path")
	public String path;

	/**
	 * Date of a document upload
	 */
	@Column(name = "uploaded")
	public LocalDate uploaded;

	public Document() {
	}

	public Document(User user, String path, LocalDate uploaded) {
		this.user = user;
		this.path = path;
		this.uploaded = uploaded;
	}

}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {Message} from '../../../dto/Message';
import {User} from '../../../dto/User';
import {FILE_URL} from '../../../../../globals';
import {ImageService} from '../../../service/image.service';
import {ImageCompressionMode} from '../../../dto/enum/ImageCompressionMode';
import {Document} from '../../../dto/Document';
import {Image} from '../../../dto/Image';
import {MessageImage} from '../../../dto/local/MessageImage';


@Component({
	selector: 'app-message',
	templateUrl: './message.component.html',
	styleUrls: [
		'./message.component.scss',
		'./message.component.forwarded.scss'
	]
})
export class MessageComponent implements OnInit {

	readonly ImageCompressionMode: typeof ImageCompressionMode = ImageCompressionMode;
	readonly ImageService: typeof ImageService = ImageService;
	readonly FILE_URL = FILE_URL;

	@Input()
	message: Message;

	@Input()
	isForwarded: boolean;

	@Input()
	me: User;

	@Output()
	openProfileEvent = new EventEmitter<User>();

	@Output()
	openImageEvent = new EventEmitter<MessageImage>();

	mine: boolean;

	constructor() {
	}

	ngOnInit() {
		this.mine = this.message.sender.id === this.me.id;
	}

	openProfile(user: User): void {
		this.openProfileEvent.emit(user);
	}

	openDocument(document: Document) {
		window.open(FILE_URL + document.path, '_blank');
	}

	openImage(image: Image) {
		let messageImage = new MessageImage();
		messageImage.image = image;
		messageImage.message = this.message;

		this.openImageEvent.emit(messageImage);
	}
}
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {API_URL} from '../../../globals';
import {Observable} from 'rxjs';
import {Avatar} from '../dto/Avatar';

@Injectable({
	providedIn: 'root'
})
export class AvatarService {

	constructor(private http: HttpClient) {
	}

	upload(token: string, file): Observable<Avatar> {
		const formData = new FormData();
		formData.append('avatar', file);

		return this.http.post<Avatar>(API_URL + 'avatar/upload', formData, {
			headers: {
				'Auth-Token': token
			}
		});
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.test.integration;

import com.gmail.ivanjermakov1.messenger.controller.ConversationController;
import com.gmail.ivanjermakov1.messenger.controller.MessageController;
import com.gmail.ivanjermakov1.messenger.controller.MessagingController;
import com.gmail.ivanjermakov1.messenger.dto.ConversationDto;
import com.gmail.ivanjermakov1.messenger.dto.MessageDto;
import com.gmail.ivanjermakov1.messenger.dto.NewMessageDto;
import com.gmail.ivanjermakov1.messenger.dto.TestingUser;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.AuthorizationException;
import com.gmail.ivanjermakov1.messenger.exception.RegistrationException;
import com.gmail.ivanjermakov1.messenger.service.TestingService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.Arrays;
import java.util.List;

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class MessageTest {

	@Autowired
	private MessageController messageController;

	@Autowired
	private ConversationController conversationController;

	@Autowired
	private MessagingController messagingController;

	@Autowired
	private TestingService testingService;

	private TestingUser user1;
	private ConversationDto conversationDto;
	private MessageDto message1;
	private MessageDto message2;

	@Before
	public void before() throws RegistrationException, AuthenticationException, AuthorizationException {
		user1 = testingService.registerUser("Jack");
		TestingUser user2 = testingService.registerUser("Ron");

		conversationDto = conversationController.create(
				user1.user,
				user2.user.login
		);

		message1 = messagingController.sendMessage(
				user1.user,
				new NewMessageDto(
						user1.user.id,
						conversationDto.id,
						"Hello!"
				)
		);

		message2 = messagingController.sendMessage(
				user1.user,
				new NewMessageDto(
						user1.user.id,
						conversationDto.id,
						"Hello2!"
				)
		);
	}

	@Test
	public void shouldGetAllMessagesFromConversation() throws AuthenticationException {
		List<MessageDto> messages = messageController.get(
				user1.user,
				conversationDto.id,
				PageRequest.of(0, Integer.MAX_VALUE)
		);

		Assert.assertEquals(2, messages.size());
	}

	@Test
	public void shouldDeleteAllMessagesFromConversation() throws AuthenticationException {
		messageController.delete(
				user1.user,
				Arrays.asList(message1, message2)
		);

		List<MessageDto> messages = messageController.get(
				user1.user,
				conversationDto.id,
				PageRequest.of(0, Integer.MAX_VALUE)
		);

		Assert.assertTrue(messages.isEmpty());
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.dto;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDateTime;

public class UserDto {

	public Long id;
	public String login;
	public String firstName;
	public String lastName;
	public AvatarDto avatar;

	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
	public LocalDateTime lastSeen;

	public UserDto() {
	}

	public UserDto(Long id, String login, String firstName, String lastName, AvatarDto avatar, LocalDateTime lastSeen) {
		this.id = id;
		this.login = login;
		this.firstName = firstName;
		this.lastName = lastName;
		this.avatar = avatar;
		this.lastSeen = lastSeen;
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.security;

import org.apache.commons.text.CharacterPredicates;
import org.jetbrains.annotations.NotNull;

public class RandomStringGenerator {

	private static org.apache.commons.text.RandomStringGenerator randomStringGenerator =
			new org.apache.commons.text.RandomStringGenerator.Builder()
					.withinRange('0', 'z')
					.filteredBy(CharacterPredicates.LETTERS, CharacterPredicates.DIGITS)
					.build();

	@NotNull
	public static String generate(@NotNull Integer length) {
		return randomStringGenerator.generate(length);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.entity;

import com.gmail.ivanjermakov1.messenger.dto.enums.MaritalStatus;

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.time.LocalDate;

/**
 * Entity represents user info
 */
@Entity
@Access(AccessType.FIELD)
@Table(name = "user_info")
public class UserInfo {

	/**
	 * User info id
	 */
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Long id;

	/**
	 * User itself
	 */
	@OneToOne
	@JoinColumn(name = "user_id")
	public User user;

	/**
	 * User first name
	 */
	@Column(name = "first_name")
	public String firstName;

	/**
	 * User last name
	 */
	@Column(name = "last_name")
	public String lastName;

	/**
	 * User gender. True is male, False is female
	 */
	@Column(name = "gender")
	public Boolean gender;

	/**
	 * User birth date
	 */
	@Column(name = "birth_date")
	public LocalDate birthDate;

	/**
	 * User marital status
	 */
	@Enumerated(EnumType.STRING)
	@Column(name = "marital_status")
	public MaritalStatus maritalStatus;

	/**
	 * User living country
	 */
	@Column(name = "country")
	public String country;

	/**
	 * User living city
	 */
	@Column(name = "city")
	public String city;

	/**
	 * User living location
	 */
	@Column(name = "location")
	public String location;

	/**
	 * User phone number in {@link String} format
	 */
	@Column(name = "phone_number")
	public String phoneNumber;

	/**
	 * User mail in {@link String} format
	 */
	@Column(name = "mail")
	public String mail;

	/**
	 * User place of education
	 */
	@Column(name = "place_of_education")
	public String placeOfEducation;

	/**
	 * User place of work
	 */
	@Column(name = "place_of_work")
	public String placeOfWork;

	/**
	 * User about info
	 */
	@Column(name = "about")
	public String about;

	public UserInfo() {
	}

	public UserInfo(User user, String firstName, String lastName) {
		this.user = user;
		this.firstName = firstName;
		this.lastName = lastName;
	}

	public UserInfo(User user, String firstName, String lastName, Boolean gender, LocalDate birthDate, MaritalStatus maritalStatus, String country, String city, String location, String phoneNumber, String mail, String placeOfEducation, String placeOfWork, String about) {
		this.user = user;
		this.firstName = firstName;
		this.lastName = lastName;
		this.gender = gender;
		this.birthDate = birthDate;
		this.maritalStatus = maritalStatus;
		this.country = country;
		this.city = city;
		this.location = location;
		this.phoneNumber = phoneNumber;
		this.mail = mail;
		this.placeOfEducation = placeOfEducation;
		this.placeOfWork = placeOfWork;
		this.about = about;
	}

}
<file_sep>rootProject.name = 'messenger'

include 'core'
include 'web'
include 'proxy'<file_sep>package com.gmail.ivanjermakov1.messenger.dto;

import com.gmail.ivanjermakov1.messenger.entity.User;

public class TestingUser {

	public User user;
	public UserDto userDto;
	public String token;

	public TestingUser() {
	}

	public TestingUser(User user, UserDto userDto, String token) {
		this.user = user;
		this.userDto = userDto;
		this.token = token;
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {API_URL} from '../../../globals';
import {NewChat} from '../dto/NewChat';
import {Conversation} from '../dto/Conversation';

@Injectable({
	providedIn: 'root'
})
export class ChatService {

	constructor(private http: HttpClient) {
	}

	create(token: string, newChat: NewChat): Observable<Conversation> {
		return this.http.post<Conversation>(API_URL + 'chat/create', newChat, {
			headers: {
				'Auth-Token': token
			}
		});
	}

	addMember(token: string, chatId: number, memberId: number): Observable<void> {
		return this.http.get<void>(API_URL + 'chat/add', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'chatId': chatId.toString(),
				'memberId': memberId.toString()
			}
		});
	}

	addMembers(token: string, chatId: number, memberIds: number[]): Observable<void> {
		return this.http.post<void>(API_URL + 'chat/add', memberIds, {
			headers: {
				'Auth-Token': token
			},
			params: {
				'chatId': chatId.toString(),
			}
		});
	}

	kickMember(token: string, chatId: number, memberId: number): Observable<void> {
		return this.http.get<void>(API_URL + 'chat/kick', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'chatId': chatId.toString(),
				'memberId': memberId.toString()
			}
		});
	}

	delete(token: string, conversationId: number) {
		return this.http.get(API_URL + 'chat/delete', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'id': conversationId.toString()
			}
		});
	}

	hide(token: string, conversationId: number) {
		return this.http.get(API_URL + 'chat/hide', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'id': conversationId.toString()
			}
		});
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.controller;

import com.gmail.ivanjermakov1.messenger.dto.PreviewDto;
import com.gmail.ivanjermakov1.messenger.dto.UserDto;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.exception.InvalidSearchFormatException;
import com.gmail.ivanjermakov1.messenger.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("search")
@Transactional
public class SearchControllerImpl implements SearchController {

	private final SearchService searchService;

	@Autowired
	public SearchControllerImpl(SearchService searchService) {
		this.searchService = searchService;
	}

	@Override
	@GetMapping("conversations")
	public List<PreviewDto> searchConversations(@ModelAttribute User user,
	                                            @RequestParam("search") String search,
	                                            Pageable pageable) {
		return searchService.searchConversations(user, search, pageable);
	}

	@Override
	@GetMapping("users")
	public List<UserDto> searchUsers(@ModelAttribute User user,
	                                 @RequestParam("search") String search,
	                                 Pageable pageable) throws InvalidSearchFormatException {
		return searchService.searchUsers(search, pageable);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.dto.enums;

public enum FileType {
	IMAGE,
	VIDEO,
	AVATAR,
	DOCUMENT
}
<file_sep>plugins {
    id 'java'
    id 'org.springframework.boot' version '2.1.1.RELEASE'

//    code coverage
    id 'jacoco'
    id 'com.github.kt3k.coveralls' version '2.8.4'
}

group 'com.gmail.ivanjermakov1'
version '0.3'

sourceCompatibility = 1.8

test {
    systemProperties System.properties
    systemProperties['user.dir'] = workingDir
}

bootRun {
    systemProperties System.properties
}

ext {
    springVersion = '2.1.8.RELEASE'
}

jacocoTestReport {
    reports {
        xml.enabled = true
        html.enabled = true
    }
}

coveralls {
    jacocoReportPath 'build/reports/jacoco/test/jacocoTestReport.xml'
}

dependencies {
    compile "org.jetbrains:annotations:16.0.2"

    compile "org.springframework.boot:spring-boot-starter-web:$springVersion"
    compile "org.springframework.boot:spring-boot-starter-webflux:$springVersion"
    compile "org.springframework.boot:spring-boot-starter-data-jpa:$springVersion"
    compile "org.springframework.boot:spring-boot-starter-mail:$springVersion"

    compile "org.postgresql:postgresql:42.2.4"
    compile "dom4j:dom4j:1.6.1"
    compile "com.atlassian.commonmark:commonmark:0.12.1"
    compile "org.modelmapper:modelmapper:2.3.0"

    implementation 'com.github.ivanjermakov:jtrue:0.3.2'

    compile "org.apache.commons:commons-io:1.3.2"
    compile "org.apache.commons:commons-text:1.6"

    compile "javax.mail:mail:1.4"
    compile "com.twilio.sdk:twilio:7.35.1"

    testCompile "org.springframework.boot:spring-boot-starter-test:$springVersion"
    testCompile "io.projectreactor:reactor-test:3.1.0.RELEASE"
    testCompile "org.jacoco:org.jacoco.agent:0.8.4"
}<file_sep>import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs';

@Injectable({
	providedIn: 'root'
})
export class BackgroundUnreadService {

	private unreadCount = new BehaviorSubject<number>(0);
	oUnreadCount = this.unreadCount.asObservable();

	constructor() {
	}

	incrementUnreadCount() {
		this.unreadCount.next(this.unreadCount.getValue() + 1);
	}

	resetUnreadCount() {
		this.unreadCount.next(0);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.service;

import com.gmail.ivanjermakov1.messenger.dto.enums.FileType;
import com.gmail.ivanjermakov1.messenger.exception.InvalidEntityException;
import com.gmail.ivanjermakov1.messenger.security.RandomStringGenerator;
import com.gmail.ivanjermakov1.messenger.util.ImageCompressionMode;
import com.gmail.ivanjermakov1.messenger.util.ImageCompressor;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;

@Service
public class FileUploadService {

	private final static Logger LOG = LoggerFactory.getLogger(FileUploadService.class);

	@Value("${fileupload.path}")
	private String uploadPlaceholder;

	public String upload(MultipartFile multipartFile, FileType fileType) throws IOException {
		String generatedFilename =
				RandomStringGenerator.generate(10) + "." + FilenameUtils.getExtension(multipartFile.getOriginalFilename());

		LOG.info("upload [" + fileType + "] \'" + multipartFile.getOriginalFilename() + "\'; size: " + multipartFile.getSize() / 1000 + "KB with name: " + generatedFilename);

		new File(uploadPlaceholder + "/" + fileType.toString().toLowerCase()).mkdirs();

		String fullFilePath = uploadPlaceholder + "/" + fileType.toString().toLowerCase() + "/" + generatedFilename;
		File file = new File(fullFilePath);
		if (file.exists()) throw new InvalidEntityException("such file already exists");
		multipartFile.transferTo(Paths.get(fullFilePath));

		uploadCompressedVersions(multipartFile, fullFilePath);

		return fileType.toString().toLowerCase() + "/" + generatedFilename;
	}

	public void uploadCompressedVersions(MultipartFile multipartFile, String filePath) throws IOException {
		String extension = "." + FilenameUtils.getExtension(filePath);
		String fileName = FilenameUtils.getBaseName(filePath);
		String path = FilenameUtils.getFullPath(filePath);
//		so {path}{fileName}.{extension} is {filePath}

		ImageCompressor.compress(filePath, path + fileName + "_" + ImageCompressionMode.MEDIUM.getFilePathMark() + extension, .4f);
		ImageCompressor.compress(filePath, path + fileName + "_" + ImageCompressionMode.SMALL.getFilePathMark() + extension, .1f);
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {API_URL} from '../../../globals';
import {HttpClient} from '@angular/common/http';
import {RegisterUser} from '../dto/RegisterUser';

@Injectable({
	providedIn: 'root'
})
export class RegisterService {

	constructor(private http: HttpClient) {
	}

	register(registerUser: RegisterUser) {
		return this.http.post(API_URL + 'register', registerUser);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.controller;

import com.gmail.ivanjermakov1.messenger.dto.ConversationDto;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;

public interface ConversationController {

	/**
	 * Create conversation with specified user.
	 * If specified @param withLogin is user himself then "self-conversation" is created.
	 *
	 * @param user      authenticated user. automatically maps, when {@literal Auth-Token} parameter present
	 * @param withLogin login of user to create conversation with
	 * @return created conversation
	 */
//	TODO: use withId instead of withLogin to be more consistent
	ConversationDto create(User user, String withLogin);

	/**
	 * Hide conversation for calling user and delete all messages sent by him.
	 *
	 * @param user authenticated user. automatically maps, when {@literal Auth-Token} parameter present
	 * @param id   id of conversation to delete
	 * @throws AuthenticationException on invalid @param token
	 */
	void delete(User user, Long id) throws AuthenticationException;

	/**
	 * Hide conversation from calling user
	 *
	 * @param user authenticated user. automatically maps, when {@literal Auth-Token} parameter present
	 * @param id   id of conversation to hide
	 */
	void hide(User user, Long id);

}
<file_sep>package com.gmail.ivanjermakov1.messenger.test.integration;

import com.gmail.ivanjermakov1.messenger.controller.RegistrationController;
import com.gmail.ivanjermakov1.messenger.dto.RegisterUserDto;
import com.gmail.ivanjermakov1.messenger.exception.InvalidEntityException;
import com.gmail.ivanjermakov1.messenger.exception.RegistrationException;
import com.google.common.base.Strings;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class PasswordTest {

	@Autowired
	private RegistrationController registrationController;

	@Test(expected = InvalidEntityException.class)
	public void shouldThrowException_WithEmptyPassword() throws RegistrationException {
		registrationController.register(
				new RegisterUserDto("Jack", "Johnson", "jackj", ""));
	}

	@Test(expected = InvalidEntityException.class)
	public void shouldThrowException_WithLessThen8Characters() throws RegistrationException {
		registrationController.register(
				new RegisterUserDto("Jack", "Johnson", "jackj", Strings.repeat("1", 7)));
	}

	@Test(expected = InvalidEntityException.class)
	public void shouldThrowException_WithMoreThen32Characters() throws RegistrationException {
		registrationController.register(
				new RegisterUserDto("Jack", "Johnson", "jackj", Strings.repeat("1", 33)));
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.controller;

import com.gmail.ivanjermakov1.messenger.dto.ConversationDto;
import com.gmail.ivanjermakov1.messenger.dto.NewChatDto;
import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.AuthorizationException;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import com.gmail.ivanjermakov1.messenger.mapper.ConversationMapper;
import com.gmail.ivanjermakov1.messenger.repository.ConversationRepository;
import com.gmail.ivanjermakov1.messenger.service.ChatService;
import com.gmail.ivanjermakov1.messenger.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("chat")
@Transactional
public class ChatControllerImpl implements ChatController {

	private final ChatService chatService;
	private final UserService userService;
	private final ConversationController conversationController;
	private final ConversationRepository conversationRepository;

	private ConversationMapper conversationMapper;

	@Autowired
	public ChatControllerImpl(UserService userService, ChatService chatService, ConversationController conversationController, ConversationRepository conversationRepository) {
		this.userService = userService;
		this.chatService = chatService;
		this.conversationController = conversationController;
		this.conversationRepository = conversationRepository;
	}

	@Autowired
	public void setConversationMapper(ConversationMapper conversationMapper) {
		this.conversationMapper = conversationMapper;
	}

	@Override
	@PostMapping("create")
	public ConversationDto create(@ModelAttribute User user,
	                              @RequestBody NewChatDto chat) {
		return conversationMapper.with(user).map(chatService.create(user, chat));
	}

	@Override
	@GetMapping("add")
	public void addMember(@ModelAttribute User user,
	                      @RequestParam("chatId") Long chatId,
	                      @RequestParam("memberId") Long memberId) throws NoSuchEntityException {
		Conversation chat = conversationRepository.findById(chatId)
				.orElseThrow(() -> new NoSuchEntityException("no such chat"));
		User member = userService.getUser(memberId);

		chatService.addMembers(user, chat, new ArrayList<>(Collections.singletonList(member)));
	}

	@Override
	@PostMapping("add")
	public void addMembers(@ModelAttribute User user,
	                       @RequestParam("chatId") Long chatId,
	                       @RequestBody List<Long> memberIds) throws NoSuchEntityException {
		Conversation chat = conversationRepository.findById(chatId)
				.orElseThrow(() -> new NoSuchEntityException("no such chat"));

		List<User> members = memberIds
				.stream()
				.map(userService::getUser)
				.collect(Collectors.toList());

		chatService.addMembers(user, chat, members);
	}

	@Override
	@GetMapping("kick")
	public void kickMember(@ModelAttribute User user,
	                       @RequestParam("chatId") Long chatId,
	                       @RequestParam("memberId") Long memberId) throws AuthorizationException, IllegalStateException {
		Conversation chat = conversationRepository.findById(chatId)
				.orElseThrow(() -> new NoSuchEntityException("no such chat"));
		User member = userService.getUser(memberId);

		chatService.kickMember(user, chat, member);
	}

	@Override
	@GetMapping("delete")
	public void delete(@ModelAttribute User user,
	                   @RequestParam("id") Long conversationId) throws AuthenticationException {
		conversationController.delete(user, conversationId);
	}

	@Override
	@GetMapping("hide")
	public void hide(@ModelAttribute User user,
	                 @RequestParam("id") Long id) {
		conversationController.hide(user, id);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.service;

import com.gmail.ivanjermakov1.messenger.dto.MessageDto;
import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import com.gmail.ivanjermakov1.messenger.entity.Message;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.entity.UserConversation;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import com.gmail.ivanjermakov1.messenger.mapper.MessageMapper;
import com.gmail.ivanjermakov1.messenger.repository.MessageRepository;
import com.gmail.ivanjermakov1.messenger.repository.UserConversationRepository;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Service
public class MessageService {

	private final MessageRepository messageRepository;
	private final UserService userService;
	private ConversationService conversationService;
	private final ImageService imageService;
	private final UserConversationRepository userConversationRepository;

	private MessageMapper messageMapper;

	@Autowired
	public MessageService(MessageRepository messageRepository, UserService userService, ImageService imageService, UserConversationRepository userConversationRepository) {
		this.messageRepository = messageRepository;
		this.userService = userService;
		this.imageService = imageService;
		this.userConversationRepository = userConversationRepository;
	}

	@Autowired
	public void setConversationService(ConversationService conversationService) {
		this.conversationService = conversationService;
	}

	@Autowired
	public void setMessageMapper(MessageMapper messageMapper) {
		this.messageMapper = messageMapper;
	}

	public List<MessageDto> get(@NotNull Long userId, @NotNull Long conversationId, Pageable pageable) {
		User user = userService.getUser(userId);
		Conversation conversation = conversationService.get(conversationId);
		UserConversation userConversation = userConversationRepository.findByUserAndConversation(user, conversation)
				.orElseThrow(() -> new NoSuchEntityException("invalid conversation id"));

		List<Message> messages = messageRepository.findAllByConversationOrderBySentDesc(
				conversation,
				PageRequest.of(0, Integer.MAX_VALUE)
		);

		return messages
				.stream()
				.filter(m -> !(userConversation.kicked && userConversation.lastRead.isBefore(m.sent)))
				.skip(pageable.getOffset())
				.limit(pageable.getPageSize())
				.map(message -> messageMapper.with(user).map(message))
				.collect(Collectors.toList());
	}

	public void read(User user, Conversation conversation) {
		UserConversation userConversation = userConversationRepository.findByUserAndConversation(user, conversation)
				.orElseThrow(() -> new NoSuchEntityException("no such user's conversation"));

		if (!userConversation.kicked) {
			userConversation.lastRead = LocalDateTime.now();
		}
	}

	/**
	 * Allowed to delete only user-self messages
	 *
	 * @param user           messages owner
	 * @param deleteMessages messages which going to be removed
	 */
	public void delete(User user, List<MessageDto> deleteMessages) {
		deleteMessages
				.stream()
				.map(dto -> messageRepository.getById(dto.id))
				.filter(Optional::isPresent)
				.map(Optional::get)
				.filter(m -> m.sender.id.equals(user.id))
				.forEach(this::delete);
	}

	public void deleteImages(Message message) {
		message.images.forEach(i -> imageService.delete(message.sender, i.id));
	}

	public Message get(Long messageId) {
		return messageRepository.getById(messageId)
				.orElseThrow(() -> new NoSuchEntityException("no such message"));
	}

	public void deleteAll(User user, Conversation conversation) {
//		TODO: optimize
		messageRepository.getAllBySenderAndConversation(user, conversation)
				.stream()
				.parallel()
				.forEach(this::delete);
	}

	/**
	 * Delete messages different way then just <code>.delete()</code>. Firstly deleting current message from all over
	 * forwarded messages (including originally attached images), then deletes itself
	 *
	 * @param message message that will be deleted
	 */
	public void delete(Message message) {
		messageRepository.deleteFromForwarded(message.id);
		deleteImages(message);
		messageRepository.delete(message);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.repository;

import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import com.gmail.ivanjermakov1.messenger.entity.Message;
import com.gmail.ivanjermakov1.messenger.entity.User;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

public interface MessageRepository extends CrudRepository<Message, Long> {

	Optional<Message> getById(Long messageId);

	Optional<Message> getTop1ByConversationOrderBySentDesc(Conversation conversation);

	List<Message> getAllBySenderAndConversation(User sender, Conversation conversation);

	List<Message> findAllByConversationOrderBySentDesc(Conversation conversation, Pageable pageable);

	@Query("select count(m) from Message m where m.conversation = :conversation and m.sender <> :user and m.sent > :lastRead")
	Integer countUnread(@Param("user") User user, @Param("conversation") Conversation conversation, @Param("lastRead") LocalDateTime lastRead);

	@Modifying
	@Transactional
	@Query(value = "delete from forwarded_message where forwarded_message_id = :id", nativeQuery = true)
	void deleteFromForwarded(@Param("id") Long id);

	@Modifying
	@Transactional
	@Query(value = "delete from forwarded_message where parent_message_id = :id", nativeQuery = true)
	void deleteForwarded(@Param("id") Long id);

}
<file_sep>import {Directive, HostListener} from '@angular/core';
import {MessagingComponent} from '../component/routed/messaging/messaging.component';

@Directive({selector: '[appMessageSend]'})
export class MessageSendDirective {

	constructor(private messagingComponent: MessagingComponent) {
	}

	@HostListener('document:keypress', ['$event'])
	handleKeydown(event) {
		if (event.code === 'Enter' && !event.shiftKey) {
			if (this.messagingComponent.editingMessage) {
				this.messagingComponent.editMessage();
			} else {
				this.messagingComponent.sendMessage();
			}
			event.preventDefault();
		}
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.dto;

public class ChannelDto {

	public Long id;
	public String name;
	public Boolean hidden;

	public ChannelDto() {
	}

	public ChannelDto(Long id, String name, Boolean hidden) {
		this.id = id;
		this.name = name;
		this.hidden = hidden;
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.util;

public class Strings {

	public static boolean startsWith(String source, String target) {
		return target.toLowerCase().startsWith(source.toLowerCase());
	}

}
<file_sep>export const API_URL = 'http://localhost:8080/api/';
export const FILE_URL = API_URL + 'file/';
export const MESSAGES_AMOUNT = 20;
export const PREVIEWS_AMOUNT = 20;
export const APP_TITLE = 'Messenger';
export const MINUTES_AS_ONLINE_LIMIT = 2;
<file_sep>import {Component, Input, OnInit} from '@angular/core';
import {FILE_URL} from '../../../../../../globals';
import {Preview} from '../../../../dto/Preview';
import {User} from '../../../../dto/User';
import {MeProvider} from '../../../../provider/me-provider';
import {ImageCompressionMode} from '../../../../dto/enum/ImageCompressionMode';
import {ImageService} from '../../../../service/image.service';
import {PreviewType} from '../../../../dto/enum/PreviewType';

@Component({
	selector: 'app-conversation-preview',
	templateUrl: './conversation-preview.component.html',
	styleUrls: ['./conversation-preview.component.scss']
})
export class ConversationPreviewComponent implements OnInit {

	readonly ImageCompressionMode: typeof ImageCompressionMode = ImageCompressionMode;
	readonly ImageService: typeof ImageService = ImageService;
	readonly FILE_URL = FILE_URL;
	readonly PreviewType: typeof PreviewType = PreviewType;

	@Input() preview: Preview;
	@Input() selected: boolean;
	@Input() isOnline: boolean;

	me: User;

	constructor(private meProvider: MeProvider) {
	}

	ngOnInit() {
		this.meProvider.oMe.subscribe(me => this.me = me);
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {BehaviorSubject} from 'rxjs';
import {User} from '../dto/User';

@Injectable({
	providedIn: 'root'
})
export class MeProvider {

	private me = new BehaviorSubject<User>(null);
	oMe = this.me.asObservable();

	constructor() {
	}

	setMe(me: User) {
		this.me.next(me);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.service;

import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Service;

@Service
public class HashServiceImpl implements HashService {

	@Override
	public String getHash(String input) {
		return DigestUtils.sha256Hex(input);
	}

	@Override
	public boolean check(String input, String stored) {
		return getHash(input).equals(stored);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.entity;

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import java.util.List;

/**
 * Entity representing conversation
 */
@Entity
@Access(AccessType.FIELD)
@Table(name = "conversation")
public class Conversation {

	/**
	 * Conversation id
	 */
	@Id
	@Column(name = "id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Long id;

	/**
	 * If conversation is chat then this field represents its name. Otherwise field is {@code null}
	 */
	@Column(name = "chat_name")
	public String chatName;

	/**
	 * List of intermediate table entities user conversations
	 */
	@OneToMany(mappedBy = "conversation", cascade = {CascadeType.ALL}, orphanRemoval = true)
	public List<UserConversation> userConversations;

	/**
	 * Creator user of conversation
	 */
	@ManyToOne
	@JoinColumn(name = "creator_id")
	public User creator;

	public Conversation() {
	}

	public Conversation(String chatName, List<UserConversation> userConversations, User creator) {
		this.chatName = chatName;
		this.userConversations = userConversations;
		this.creator = creator;
	}

}
<file_sep>import {Message} from './Message';
import {Image} from './Image';

export class EditMessage {
	id: number;
	text: string;
	forwarded: Message[];
	images: Image[];
}
<file_sep>package com.gmail.ivanjermakov1.messenger.util;

import org.apache.commons.io.IOUtils;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class Files {

	public static MultipartFile multipartFileFromFile(File file) throws IOException {
		FileInputStream input = new FileInputStream(file);
		return new MockMultipartFile(file.getName(),
				file.getName(),
				"text/plain",
				IOUtils.toByteArray(input)
		);
	}

}
<file_sep>import {User} from './User';
import {Conversation} from './Conversation';
import {Message} from './Message';
import {PreviewType} from './enum/PreviewType';
import {Avatar} from './Avatar';

export class Preview {
	type: PreviewType;
	conversation: Conversation;
	with: User;
	lastMessage: Message;
	avatar: Avatar;
	unread: number;
	kicked: boolean;
}
<file_sep>import {Message} from './Message';

export class MessageAttachments {

	forwarded: Message[] = [];

}
<file_sep>import {Injectable} from '@angular/core';

@Injectable({
	providedIn: 'root'
})
export class SoundNotificationService {

	constructor() {
	}

	notify() {
		const notificationAudio = new Audio('assets/sound/newmsg.mp3');
		let promise = notificationAudio.play();

		// required due to browser policy
		// described here: https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
		if (promise) {
			promise.catch(e => {
			});
		}
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {API_URL} from '../../../globals';
import {NewImage} from '../dto/NewImage';
import {ImageCompressionMode} from '../dto/enum/ImageCompressionMode';

@Injectable({
	providedIn: 'root'
})
export class ImageService {

	constructor(private http: HttpClient) {
	}

	upload(token: string, file): Observable<NewImage> {
		const formData = new FormData();
		formData.append('image', file);

		return this.http.post<NewImage>(API_URL + 'image/upload', formData, {
			headers: {
				'Auth-Token': token
			}
		});
	}

	delete(token: string, imageId: number): Observable<void> {
		return this.http.get<void>(API_URL + 'image/delete', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'imageId': imageId.toString()
			}
		});
	}

	static getImagePathByCompressionMode(path: string, compressionMode: ImageCompressionMode): string {
		if (compressionMode === ImageCompressionMode.FULL) return path;

		const pathName = path.substring(0, path.lastIndexOf('.'));
		const extension = path.substring(path.lastIndexOf('.'));
		return `${pathName}_${compressionMode.toString()}${extension}`;
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.dto;

import com.gmail.ivanjermakov1.messenger.dto.enums.PreviewType;

public class PreviewDto {

	public PreviewType type;
	public ConversationDto conversation;
	public ChannelDto channel;
	public UserDto with;
	public MessageDto lastMessage;
	public AvatarDto avatar;
	public Integer unread;
	public Boolean kicked;

	public PreviewDto() {
	}

	public PreviewDto(PreviewType type, ConversationDto conversation, ChannelDto channel, UserDto with, MessageDto lastMessage, AvatarDto avatar, Integer unread, Boolean kicked) {
		this.type = type;
		this.conversation = conversation;
		this.channel = channel;
		this.with = with;
		this.lastMessage = lastMessage;
		this.avatar = avatar;
		this.unread = unread;
		this.kicked = kicked;
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {API_URL} from '../../../globals';
import {Observable} from 'rxjs';
import {UserInfo} from '../dto/UserInfo';

@Injectable({
	providedIn: 'root'
})
export class UserInfoService {

	constructor(private http: HttpClient) {
	}

	get(token: string, userId: number): Observable<UserInfo> {
		return this.http.get<UserInfo>(API_URL + 'info', {
			headers: {
				'Auth-Token': token
			},
			params: {
				userId: userId.toString()
			}
		});
	}

	edit(token: string, userInfo: UserInfo): Observable<UserInfo> {
		return this.http.post<UserInfo>(API_URL + 'info', userInfo, {
			headers: {
				'Auth-Token': token
			}
		});
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.util;

import org.modelmapper.ModelMapper;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

public class Mappers {

	private static ModelMapper modelMapper;

	static {
		modelMapper = new ModelMapper();
		modelMapper
				.getConfiguration()
				.setFieldMatchingEnabled(true)
				.setSkipNullEnabled(true);
	}

	public static <D, T> D map(T entity, Class<D> outClass) {
		return modelMapper.map(entity, outClass);
	}

	public static <D, T> List<D> mapAll(Collection<T> entities, Class<D> outCLass) {
		return entities
				.stream()
				.map(e -> map(e, outCLass))
				.collect(Collectors.toList());
	}

}<file_sep>import {Injectable} from '@angular/core';
import {API_URL} from '../../../globals';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {User} from '../dto/User';

@Injectable({
	providedIn: 'root'
})
export class AuthService {

	constructor(private http: HttpClient) {
	}

	authenticate(login: string, password: string): Observable<string> {
		return this.http.get(API_URL + 'auth', {
			params: {'login': login, 'password': <PASSWORD>},
			responseType: 'text'
		});
	}

	validate(token: string): Observable<User> {
		return this.http.get<User>(API_URL + 'auth/validate', {
			headers: {
				'Auth-Token': token
			}
		});
	}

	logout(token: string) {
		return this.http.get<User>(API_URL + 'auth/logout', {
			headers: {
				'Auth-Token': token
			}
		});
	}

}
<file_sep>import {User} from './User';

export class Image {
	id: number;
	user: User;
	path: string;
	uploaded: Date;
}
<file_sep>package com.gmail.ivanjermakov1.messenger.dto;

public class NewImageDto {

	public String path;

	public NewImageDto() {
	}

	public NewImageDto(String path) {
		this.path = path;
	}

}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {Message} from '../../../../dto/Message';

@Component({
	selector: 'app-forwarded-attachment',
	templateUrl: './forwarded-attachment.component.html',
	styleUrls: [
		'./forwarded-attachment.component.scss',
		'./../attachment.scss',
	]
})
export class ForwardedAttachmentComponent implements OnInit {

	@Input() forwarded: Message[];
	@Output() removeForwardedAttachmentEvent = new EventEmitter();

	constructor() {
	}

	ngOnInit() {
	}

	removeForwardedAttachment() {
		this.removeForwardedAttachmentEvent.next();
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.util;

import org.apache.commons.io.FilenameUtils;
import org.springframework.web.multipart.MultipartFile;

import java.util.Arrays;

public class Uploads {

	public static boolean isSupportedImage(MultipartFile file) {
		if (file == null || file.getOriginalFilename() == null) return false;

		return Arrays.stream(ImageFileType.values())
				.anyMatch(e -> e.toString().toLowerCase().equals(FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase()));
	}

}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {User} from '../../../../dto/User';
import {ImageCompressionMode} from '../../../../dto/enum/ImageCompressionMode';
import {ImageService} from '../../../../service/image.service';
import {FILE_URL} from '../../../../../../globals';
import {Preview} from '../../../../dto/Preview';
import {PreviewType} from '../../../../dto/enum/PreviewType';

@Component({
	selector: 'app-user-preview',
	templateUrl: './user-preview.component.html',
	styleUrls: ['./user-preview.component.scss']
})
export class UserPreviewComponent implements OnInit {

	readonly ImageCompressionMode: typeof ImageCompressionMode = ImageCompressionMode;
	readonly ImageService: typeof ImageService = ImageService;
	readonly FILE_URL = FILE_URL;
	readonly PreviewType: typeof PreviewType = PreviewType;

	@Input() user: User;
	@Input() currentPreview: Preview;
	@Output() addToChatEvent = new EventEmitter<User>();

	constructor() {
	}

	ngOnInit() {
	}

}
<file_sep>import {Message} from '../Message';
import {Image} from '../Image';

export class MessageImage {
	message: Message;
	image: Image;
}
<file_sep>import {Message} from './Message';
import {NewImage} from './NewImage';
import {NewDocument} from './NewDocument';

export class NewMessage {
	senderId: number;
	conversationId: number;
	text: string;
	forwarded: Message[] = [];
	images: NewImage[] = [];
	documents: NewDocument[] = [];
}
<file_sep>import {Directive, EventEmitter, HostListener, Output} from '@angular/core';

@Directive({selector: '[appScroll]'})
export class ScrollPositionDirective {

	@Output('appScrollTop') scrollTop = new EventEmitter();
	@Output('appScrollBottom') scrollBottom = new EventEmitter();

	constructor() {
	}

	@HostListener('scroll', ['$event'])
	handleScroll(event) {
		if (event.target.scrollTop === 0) this.scrollTop.emit();
		if (event.target.scrollTop === event.target.scrollHeight - event.target.clientHeight) this.scrollBottom.emit();
	}

}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {NewImage} from '../../../../dto/NewImage';
import {ImageCompressionMode} from '../../../../dto/enum/ImageCompressionMode';
import {ImageService} from '../../../../service/image.service';
import {FILE_URL} from '../../../../../../globals';

@Component({
	selector: 'app-image-attachment',
	templateUrl: './image-attachment.component.html',
	styleUrls: [
		'./image-attachment.component.scss',
		'./../attachment.scss',
	]
})
export class ImageAttachmentComponent implements OnInit {

	readonly ImageCompressionMode: typeof ImageCompressionMode = ImageCompressionMode;
	readonly ImageService: typeof ImageService = ImageService;
	readonly FILE_URL = FILE_URL;

	@Input() image: NewImage;
	@Output() removeImageAttachment = new EventEmitter();

	constructor() {
	}

	ngOnInit() {
	}

	remove() {
		this.removeImageAttachment.next();
	}

}
<file_sep>SELECT setval('avatar_id_seq', (select id from avatar order by id desc limit 1));
SELECT setval('conversation_id_seq', (select id from conversation order by id desc limit 1));
SELECT setval('document_id_seq', (select id from document order by id desc limit 1));
SELECT setval('image_id_seq', (select id from image order by id desc limit 1));
SELECT setval('message_id_seq', (select id from message order by id desc limit 1));
SELECT setval('token_id_seq', (select id from token order by id desc limit 1));
SELECT setval('user_conversation_id_seq', (select id from user_conversation order by id desc limit 1));
SELECT setval('user_id_seq', (select id from "user" order by id desc limit 1));
SELECT setval('user_info_id_seq', (select id from user_info order by id desc limit 1));
SELECT setval('user_online_id_seq', (select id from user_online order by id desc limit 1));<file_sep>export enum PreviewType {
	CONVERSATION,
	CHAT,
	CHANNEL
}
<file_sep>import {Conversation} from '../Conversation';
import {User} from '../User';

export class ConversationReadAction {
	conversation: Conversation;
	reader: User;
}
<file_sep>import {Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
import {AppComponent} from '../../../../app.component';
import {TokenProvider} from '../../../../provider/token-provider';
import {ImageService} from '../../../../service/image.service';
import {NewImage} from '../../../../dto/NewImage';
import {Message} from '../../../../dto/Message';
import {NewDocument} from '../../../../dto/NewDocument';
import {DocumentService} from '../../../../service/document.service';

@Component({
	selector: 'app-attachments-menu',
	templateUrl: './attachments-menu.component.html',
	styleUrls: ['./attachments-menu.component.scss']
})
export class AttachmentsMenuComponent implements OnInit {

	@Input() editingMessage: Message;
	@Output() attachedImages = new EventEmitter<NewImage>();
	@Output() attachedDocuments = new EventEmitter<NewDocument>();
	@ViewChild('imageInput') imageInput: ElementRef;
	@ViewChild('documentInput') documentInput: ElementRef;

	visible = false;

	private token: string;

	constructor(
		private app: AppComponent,
		private tokenProvider: TokenProvider,
		private imageService: ImageService,
		private documentService: DocumentService
	) {
	}

	ngOnInit() {
		this.app.onLoad(() => {
			this.tokenProvider.oToken.subscribe(token => {
				this.token = token;
			});
		});
	}

	selectImage() {
		this.imageInput.nativeElement.click();
	}

	selectDocument() {
		this.documentInput.nativeElement.click();
	}

	onImageSelect() {
		this.visible = false;
		Array.from(this.imageInput.nativeElement.files).forEach(f =>
			this.imageService.upload(this.token, f).subscribe(newImage => {
				this.attachedImages.emit(newImage);
			}, err => {
			})
		);
	}

	onDocumentSelect() {
		this.visible = false;
		Array.from(this.documentInput.nativeElement.files).forEach(f =>
			this.documentService.upload(this.token, f).subscribe(newDocument => {
				this.attachedDocuments.emit(newDocument);
			}, err => {
			})
		);
	}
}
<file_sep>package com.gmail.ivanjermakov1.messenger.dto;

import com.fasterxml.jackson.annotation.JsonFormat;

import java.time.LocalDate;

public class DocumentDto {

	public Long id;
	public UserDto user;
	public String path;

	@JsonFormat(pattern = "yyyy-MM-dd")
	public LocalDate uploaded;

	public DocumentDto() {
	}

	public DocumentDto(Long id, UserDto user, String path, LocalDate uploaded) {
		this.id = id;
		this.user = user;
		this.path = path;
		this.uploaded = uploaded;
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.service;


import com.twilio.Twilio;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class SmsServiceImpl implements SmsService {

	private final static Logger LOG = LoggerFactory.getLogger(SmsServiceImpl.class);

	@Value("${twilio.account.sid}")
	private String accountSid;

	@Value("${twilio.auth.token}")
	private String token;

	@Value("${twilio.from.number}")
	private String from;

	@Override
	public void send(String to, String content) {
		LOG.debug("sending sms to @" + to);

		Twilio.init(accountSid, token);
		Message message = Message.creator(new PhoneNumber(from), new PhoneNumber(to), content).create();

		LOG.info("sent sms to @" + to);
	}

}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {Conversation} from '../../../dto/Conversation';
import {ImageCompressionMode} from '../../../dto/enum/ImageCompressionMode';
import {ImageService} from '../../../service/image.service';
import {FILE_URL} from '../../../../../globals';
import {PreviewType} from '../../../dto/enum/PreviewType';
import {User} from '../../../dto/User';

@Component({
	selector: 'app-chat-info',
	templateUrl: './chat-info.component.html',
	styleUrls: [
		'./chat-info.component.scss',
		'./../profile/profile.component.scss',
		'./../preview/user-preview/user-preview.component.scss'
	]
})
export class ChatInfoComponent implements OnInit {

	readonly ImageCompressionMode: typeof ImageCompressionMode = ImageCompressionMode;
	readonly ImageService: typeof ImageService = ImageService;
	readonly FILE_URL = FILE_URL;
	readonly PreviewType: typeof PreviewType = PreviewType;

	@Input()
	chat: Conversation;

	@Output()
	close = new EventEmitter();

	@Output()
	openProfile = new EventEmitter<User>();

	@Output()
	kickMember = new EventEmitter<User>();

	constructor() {
	}

	ngOnInit() {
	}

}
<file_sep>import {User} from './User';
import {MaritalStatus} from './enum/MaritalStatus';
import {Avatar} from './Avatar';

export class UserInfo {

	user: User;
	firstName: string;
	lastName: string;
	gender: boolean;
	birthDate: Date;
	maritalStatus: MaritalStatus;
	country: string;
	city: string;
	location: string;
	phoneNumber: string;
	mail: string;
	placeOfEducation: string;
	placeOfWork: string;
	about: string;
	avatars: Avatar[];
	newAvatar: File;

}
<file_sep>import {HttpParams} from '@angular/common/http';
import {Direction} from './enum/SortDirection';

export class Pageable {

	page: number;
	size: number;
	sort: string;
	direction: Direction;

	constructor(page: number, size: number, sort?: string, direction?: Direction) {
		this.page = page;
		this.size = size;
		this.sort = sort;
		this.direction = direction;
	}

	toHttpParams(): HttpParams {
		let params = new HttpParams()
			.append('page', this.page.toString())
			.append('size', this.size.toString());

		params = this.sort && this.direction !== null
			? params.append('sort', `${this.sort},${Direction[this.direction]}`)
			: params;

		return params;
	}
}
<file_sep>import {Injectable} from '@angular/core';
import {MINUTES_AS_ONLINE_LIMIT} from '../../../globals';

import * as moment from 'moment';

@Injectable({
	providedIn: 'root'
})
export class DateService {

	constructor() {
	}

	static lastSeenView(lastSeen: Date): string {
		if (!lastSeen) return 'Offline';

		const now = moment();
		const time = moment(lastSeen);

		const minDiff = now.diff(time, 'minutes');
		if (minDiff < MINUTES_AS_ONLINE_LIMIT) return 'Online';

		if (minDiff < 60) return 'Seen ' + minDiff + ' minutes ago';

		const hourDiff = now.diff(time, 'hours');

		if (hourDiff < 24) return 'Seen today at ' + time.format('hh:mm');

		return hourDiff < 48 ? 'Seen tomorrow at ' + time.format('hh:mm') : time.format('[Seen ] MMM Do [ at ] hh:mm');
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.controller;

import com.gmail.ivanjermakov1.messenger.dto.NewImageDto;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.exception.AuthorizationException;
import com.gmail.ivanjermakov1.messenger.exception.InvalidEntityException;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

public interface ImageController {

	/**
	 * Upload image.
	 *
	 * @param user  authenticated user. automatically maps, when {@literal Auth-Token} parameter present
	 * @param image multipart image file
	 * @return uploaded image
	 * @throws IOException            on server file system error
	 * @throws InvalidEntityException on upload of invalid file (mostly caused by invalid file extension or file size
	 *                                specified in @value {@code spring.servlet.multipart.max-file-size})
	 */
	NewImageDto upload(User user, MultipartFile image) throws IOException;

	/**
	 * Delete image from certain message
	 *
	 * @param user    authenticated user. automatically maps, when {@literal Auth-Token} parameter present
	 * @param imageId image id to delete
	 * @throws NoSuchEntityException  on invalid image id
	 * @throws AuthorizationException if user is not an a sender of a message image attached to
	 */
	void delete(User user, Long imageId);

}
<file_sep>package com.gmail.ivanjermakov1.messenger.mapper;

import com.gmail.ivanjermakov1.messenger.dto.AvatarDto;
import com.gmail.ivanjermakov1.messenger.dto.UserInfoDto;
import com.gmail.ivanjermakov1.messenger.entity.UserInfo;
import com.gmail.ivanjermakov1.messenger.service.AvatarService;
import com.gmail.ivanjermakov1.messenger.util.Mappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class UserInfoMapper implements Mapper<UserInfo, UserInfoDto> {

	private AvatarService avatarService;
	private UserMapper userMapper;

	@Autowired
	public void setAvatarService(AvatarService avatarService) {
		this.avatarService = avatarService;
	}

	@Autowired
	public void setUserMapper(UserMapper userMapper) {
		this.userMapper = userMapper;
	}

	@Override
	public UserInfoDto map(UserInfo userInfo) {
		UserInfoDto userInfoDto = Mappers.map(userInfo, UserInfoDto.class);
		userInfoDto.avatars = Mappers.mapAll(avatarService.getAll(userInfo.user), AvatarDto.class);
		userInfoDto.user = userMapper.map(userInfo.user);
		return userInfoDto;
	}

}
<file_sep>import {User} from './User';
import {Conversation} from './Conversation';
import {Image} from './Image';
import {Document} from './Document';

export class Message {
	id: number;
	sent: Date;
	text: string;
	read: boolean;
	sender: User;
	conversation: Conversation;
	forwarded: Message[];
	images: Image[];
	documents: Document[];
	selected = false;
}
<file_sep>package com.gmail.ivanjermakov1.messenger.mapper;

import com.gmail.ivanjermakov1.messenger.dto.DocumentDto;
import com.gmail.ivanjermakov1.messenger.dto.ImageDto;
import com.gmail.ivanjermakov1.messenger.dto.MessageDto;
import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import com.gmail.ivanjermakov1.messenger.entity.Message;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.entity.UserConversation;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import com.gmail.ivanjermakov1.messenger.repository.MessageRepository;
import com.gmail.ivanjermakov1.messenger.repository.UserConversationRepository;
import com.gmail.ivanjermakov1.messenger.service.ConversationService;
import com.gmail.ivanjermakov1.messenger.service.UserService;
import com.gmail.ivanjermakov1.messenger.util.Mappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.stream.Collectors;

@Component
public class MessageMapper implements Mapper<Message, MessageDto>, MapperBuilder<User> {

	private UserConversationRepository userConversationRepository;
	private ConversationService conversationService;
	private ConversationMapper conversationMapper;
	private UserService userService;
	private UserMapper userMapper;
	private MessageRepository messageRepository;

	private User user;

	@Autowired
	public void setUserConversationRepository(UserConversationRepository userConversationRepository) {
		this.userConversationRepository = userConversationRepository;
	}

	@Autowired
	public void setConversationService(ConversationService conversationService) {
		this.conversationService = conversationService;
	}

	@Autowired
	public void setConversationMapper(ConversationMapper conversationMapper) {
		this.conversationMapper = conversationMapper;
	}

	@Autowired
	public void setUserService(UserService userService) {
		this.userService = userService;
	}

	@Autowired
	public void setUserMapper(UserMapper userMapper) {
		this.userMapper = userMapper;
	}

	@Autowired
	public void setMessageRepository(MessageRepository messageRepository) {
		this.messageRepository = messageRepository;
	}

	@Override
	public MessageDto map(Message message) {
		if (message.sender.id.equals(0L)) {
			return new MessageDto(
					message.id,
					message.sent,
					message.text,
					true,
					userMapper.map(message.sender),
					conversationMapper.with(user).map(message.conversation),
					Collections.emptyList(),
					Collections.emptyList(),
					Collections.emptyList()
			);
		}

		UserConversation userConversation = userConversationRepository.findByUserAndConversation(user, message.conversation)
				.orElseThrow(() -> new NoSuchEntityException("no such user's conversation"));

		MessageDto messageDto = new MessageDto();
		messageDto.id = message.id;
		messageDto.sent = message.sent;
		boolean read = userConversation.conversation.userConversations
				.stream()
				.filter(uc -> !uc.user.id.equals(user.id))
				.anyMatch(uc -> uc.lastRead.isAfter(message.sent));
		if (userConversation.conversation.userConversations.size() == 1) read = true;
		messageDto.read = read;
		messageDto.text = message.text;

		Conversation conversation = conversationService.get(message.conversation.id);
		messageDto.conversation = conversationMapper
				.with(message.sender)
				.map(conversation);

		User sender = userService.getUser(message.sender.id);
		messageDto.sender = userMapper.map(sender);

		messageDto.forwarded = messageRepository.getById(message.id)
				.map(m -> m.forwarded)
				.orElse(Collections.emptyList())
				.stream()
				.map(m -> this.with(user).map(m))
				.collect(Collectors.toList());

		messageDto.images = Mappers.mapAll(
				message.images,
				ImageDto.class
		);

		messageDto.documents = Mappers.mapAll(
				message.documents,
				DocumentDto.class
		);

		return messageDto;
	}

	@Override
	public Mapper<Message, MessageDto> with(User user) {
		this.user = user;
		return this;
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.mapper;

import com.gmail.ivanjermakov1.messenger.dto.AvatarDto;
import com.gmail.ivanjermakov1.messenger.dto.PreviewDto;
import com.gmail.ivanjermakov1.messenger.dto.enums.PreviewType;
import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.service.ConversationService;
import com.gmail.ivanjermakov1.messenger.service.MessageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;

@Component
public class PreviewMapper implements Mapper<Conversation, PreviewDto>, MapperBuilder<User> {

	@Value("${default.avatar.chat.path}")
	private String defaultAvatarChatPath;

	private MessageService messageService;
	private ConversationService conversationService;

	private UserMapper userMapper;
	private ConversationMapper conversationMapper;

	private User user;

	@Autowired
	public void setMessageService(MessageService messageService) {
		this.messageService = messageService;
	}

	@Autowired
	public void setConversationService(ConversationService conversationService) {
		this.conversationService = conversationService;
	}

	@Autowired
	public void setUserMapper(UserMapper userMapper) {
		this.userMapper = userMapper;
	}

	@Autowired
	public void setConversationMapper(ConversationMapper conversationMapper) {
		this.conversationMapper = conversationMapper;
	}

	@Override
	public PreviewDto map(Conversation conversation) {
		PreviewDto previewDto = new PreviewDto();

		previewDto.type = conversation.chatName == null ? PreviewType.CONVERSATION : PreviewType.CHAT;

		previewDto.conversation = conversationMapper
				.with(user)
				.map(conversation);

		previewDto.lastMessage = messageService.get(user.id, conversation.id, PageRequest.of(0, 1))
				.stream()
				.findFirst()
				.orElse(null);

		if (previewDto.type.equals(PreviewType.CONVERSATION)) {
			User with = conversation.userConversations
					.stream()
					.map(uc -> uc.user)
					.filter(u -> !u.id.equals(user.id))
					.findFirst()
					.orElse(user);
			previewDto.with = userMapper.map(with);
		}

		if (previewDto.type.equals(PreviewType.CONVERSATION)) {
			previewDto.avatar = previewDto.with.avatar;
		} else {
			previewDto.avatar = new AvatarDto(null, defaultAvatarChatPath, null);
		}

		if (previewDto.type.equals(PreviewType.CHAT)) {
			previewDto.kicked = conversation.userConversations
					.stream()
					.filter(uc -> uc.user.id.equals(user.id))
					.findFirst()
					.map(uc -> uc.kicked)
					.orElse(null);
		}

		previewDto.unread = conversationService.unreadCount(user, conversation);

		return previewDto;
	}

	@Override
	public Mapper<Conversation, PreviewDto> with(User user) {
		this.user = user;
		return this;
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;

@Service
public class MailService {

	private final static Logger LOG = LoggerFactory.getLogger(MailService.class);

	@Value("${mail.from}")
	private String from;

	private final JavaMailSender javaMailSender;

	@Autowired
	public MailService(JavaMailSender javaMailSender) {
		this.javaMailSender = javaMailSender;
	}

	public void send(String to, String subject, String content) {
		LOG.info("Sending message to @", to);

		SimpleMailMessage message = new SimpleMailMessage();
		message.setFrom(from);
		message.setTo(to);
		message.setSubject(subject);
		message.setText(content);
		javaMailSender.send(message);

		LOG.info("Sent message to @", to);
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {API_URL} from '../../../globals';
import {Preview} from '../dto/Preview';
import {Pageable} from '../dto/Pageable';

@Injectable({
	providedIn: 'root'
})
export class PreviewService {

	constructor(private http: HttpClient) {
	}

	all(token: string, pageable: Pageable): Observable<Preview[]> {
		return this.http.get<Preview[]>(API_URL + 'preview/all', {
			headers: {
				'Auth-Token': token
			},
			params: pageable.toHttpParams()
		});
	}

	get(token: string, conversationId: number): Observable<Preview> {
		return this.http.get<Preview>(API_URL + 'preview/get', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'conversationId': conversationId.toString()
			}
		});
	}

}
<file_sep>import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {RegisterUser} from '../../../dto/RegisterUser';
import {RegisterService} from '../../../service/register.service';

@Component({
	selector: 'app-register',
	templateUrl: './register.component.html',
	styleUrls: ['./register.component.scss', './../auth/auth.component.scss']
})
export class RegisterComponent implements OnInit {

	registerUser: RegisterUser = new RegisterUser();
	passwordConfirmation = '';

	constructor(private router: Router,
	            private registerService: RegisterService) {
	}

	ngOnInit() {
	}

	register() {
		if (this.registerUser.password === this.passwordConfirmation) {
			this.registerService.register(this.registerUser).subscribe(() => {
				this.router.navigate(['/auth']);
			}, error => {
			});
		}
	}
}
<file_sep>package com.gmail.ivanjermakov1.messenger.repository;

import com.gmail.ivanjermakov1.messenger.entity.UserOnline;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;

public interface UserOnlineRepository extends CrudRepository<UserOnline, Long> {

	@Query(value = "select * from user_online " +
			"where user_id = :userId " +
			"order by seen desc " +
			"limit 1", nativeQuery = true)
	UserOnline lastSeen(@Param("userId") Long userId);

	UserOnline findFirstByUserIdOrderBySeenDesc(Long user_id);

	@Modifying
	@Transactional
	@Query(value = "delete from user_online uo where extract(day from now() - uo.seen) >= :days", nativeQuery = true)
	void deleteOlderThanDays(@Param("days") Integer onlineLifetimeDays);

}
<file_sep>package com.gmail.ivanjermakov1.messenger.test.integration;

import com.gmail.ivanjermakov1.messenger.controller.ConversationController;
import com.gmail.ivanjermakov1.messenger.controller.PreviewController;
import com.gmail.ivanjermakov1.messenger.dto.ConversationDto;
import com.gmail.ivanjermakov1.messenger.dto.PreviewDto;
import com.gmail.ivanjermakov1.messenger.dto.TestingUser;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.AuthorizationException;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import com.gmail.ivanjermakov1.messenger.exception.RegistrationException;
import com.gmail.ivanjermakov1.messenger.service.TestingService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class ConversationTest {

	@Autowired
	private ConversationController conversationController;

	@Autowired
	private PreviewController previewController;

	@Autowired
	private TestingService testingService;

	private TestingUser user1;
	private TestingUser user2;
	private ConversationDto conversationDto;

	@Before
	public void before() {
		user1 = testingService.registerUser("Jack");
		user2 = testingService.registerUser("Ron");

		conversationDto = conversationController.create(
				user1.user,
				user2.user.login
		);
	}

	@Test
	public void shouldCreateConversation() throws RegistrationException, AuthenticationException, NoSuchEntityException, AuthorizationException {
		Assert.assertNotNull(conversationDto);
		Assert.assertEquals(2, conversationDto.users.size());
	}

	@Test
	public void shouldCreateSelfConversation() throws RegistrationException, AuthenticationException, NoSuchEntityException, AuthorizationException {
		ConversationDto self = conversationController.create(
				user1.user,
				user1.user.login
		);

		Assert.assertNotNull(self);
		Assert.assertEquals(1, self.users.size());
	}

	@Test
	public void shouldDeleteConversation() throws RegistrationException, AuthenticationException {
		conversationController.delete(user1.user, conversationDto.id);

		PreviewDto previewDto = previewController.get(user1.user, conversationDto.id);
		Assert.assertTrue(previewDto.conversation.hidden);
	}

	@Test
	public void shouldHideConversation() throws RegistrationException, AuthenticationException {
		conversationController.hide(user1.user, conversationDto.id);

		PreviewDto previewDto = previewController.get(user1.user, conversationDto.id);
		Assert.assertTrue(previewDto.conversation.hidden);
	}

	@Test
	public void shouldCreateHiddenConversation() {
		shouldHideConversation();

		conversationDto = conversationController.create(
				user1.user,
				user2.user.login
		);

		PreviewDto previewDto = previewController.get(user1.user, conversationDto.id);
		Assert.assertFalse(previewDto.conversation.hidden);
	}

}
<file_sep>import {Component} from '@angular/core';
import {CookieService} from './service/cookie.service';
import {AuthService} from './service/auth.service';
import {ActivatedRoute, NavigationEnd, Router} from '@angular/router';
import {TokenProvider} from './provider/token-provider';
import {MeProvider} from './provider/me-provider';

@Component({
	selector: 'app-root',
	templateUrl: './app.component.html',
	styleUrls: ['./app.component.scss']
})
export class AppComponent {

	isLoaded: boolean = false;

	constructor(private cookieService: CookieService,
	            private authService: AuthService,
	            private tokenProvider: TokenProvider,
	            private meProvider: MeProvider,
	            private router: Router,
	            private route: ActivatedRoute) {
		this.autoLogin();
	}

	private autoLogin() {
		this.router.events.subscribe(
			(event: any) => {
				if (event instanceof NavigationEnd) {
					if (this.router.url === '/auth' || this.router.url === '/register') {
						return;
					}

					const token = this.cookieService.getToken();
					if (token !== null) {
						this.authService.validate(token).subscribe(
							user => {
								this.tokenProvider.setToken(token);
								this.meProvider.setMe(user);
								this.isLoaded = true;

								this.route.queryParams.subscribe(params => {
									if (params['id']) {
										this.router.navigate(['/im'], {queryParams: {id: params['id']}, replaceUrl: true});
									} else {
										this.router.navigate(['/im'], {replaceUrl: true});
									}
								});
							},
							error => {
								this.router.navigate(['/auth']);
							}
						);
					} else {
						this.router.navigate(['/auth']);
					}

				}
			}
		);
	}

	onLoad(loaded: () => void) {
		setTimeout(() => {
			if (this.isLoaded) {
				loaded();
			} else {
				this.onLoad(loaded);
			}
		}, 10);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.exception;

import org.springframework.http.HttpStatus;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.time.ZoneOffset;

public class WebException {

	public Long timestamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
	public Integer status;
	public String error;
	public String message;
	public String path;

	public WebException(Integer status, String error, String message, String path) {
		this.status = status;
		this.error = error;
		this.message = message;
		this.path = path;
	}

	public static Builder builder() {
		return new Builder();
	}

	public static class Builder {

		public HttpStatus status;
		public String message;
		public String path;

		public HttpServletRequest request;
		public Exception exception;

		public Builder status(HttpStatus status) {
			this.status = status;
			return this;
		}

		public Builder message(String message) {
			this.message = message;
			return this;
		}

		public Builder path(String path) {
			this.path = path;
			return this;
		}

		public Builder request(HttpServletRequest request) {
			this.request = request;
			return this;
		}

		public Builder exception(Exception exception) {
			this.exception = exception;
			return this;
		}

		public WebException build() {
			WebException webException = new WebException(
					status.value(),
					status.getReasonPhrase(),
					message,
					path
			);

			if (request != null) {
				webException.path = request.getRequestURI();
			}

			if (exception != null) {
				webException.message = exception.getMessage();
			}

			return webException;
		}

	}


}
<file_sep><div class="preview" [ngClass]="{'selected': selected}" *ngIf="preview.lastMessage">
	<img class="avatar"
	     [src]="ImageService.getImagePathByCompressionMode(FILE_URL + preview.avatar.path, ImageCompressionMode.SMALL)"
	     alt="">
	<div class="info">
		<div class="text"
		     *ngIf="!preview.conversation.chatName">{{preview.with.firstName}} {{preview.with.lastName}}</div>
		<div class="text" *ngIf="preview.conversation.chatName">{{preview.conversation.chatName}}</div>
		<i class="fas fa-circle" *ngIf="isOnline"></i>
	</div>
	<div class="message-preview">
		<div class="from">
			{{preview.lastMessage.sender.login === me.login ? 'You' :
			preview.lastMessage.sender.firstName}}:
		</div>
		<div class="text">{{preview.lastMessage.text ? preview.lastMessage.text : 'forwarded messages'}}</div>
	</div>
	<!--<div class="delete"><i class="fas fa-times"></i></div>-->
	<div class="unread-wrapper">
		<div class="unread" *ngIf="preview.lastMessage.sender.login !== me.login && preview.unread !== 0">
			{{preview.unread}}
		</div>
		<div class="unread-mine" *ngIf="!preview.lastMessage.read && preview.lastMessage.sender.login === me.login">
			<i class="fas fa-circle"></i>
		</div>
	</div>
	<div class="sent">{{preview.lastMessage.sent | date:'HH:mm'}}</div>
</div>
<file_sep>import {User} from './User';
import {MaritalStatus} from './enum/MaritalStatus';
import {Avatar} from './Avatar';

export class EditUserInfo {
	user: User;
	firstName: string;
	lastName: string;
	gender: boolean;
	birthDate: string;
	maritalStatus: MaritalStatus;
	country: string;
	city: string;
	location: string;
	phoneNumber: string;
	mail: string;
	placeOfEducation: string;
	placeOfWork: string;
	about: string;
	avatars: Avatar[];
	newAvatar: File;
}
<file_sep>package com.gmail.ivanjermakov1.messenger.exception;

public class NoSuchEntityException extends RuntimeException {

	public NoSuchEntityException() {
	}

	public NoSuchEntityException(String message) {
		super(message);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.controller;

import com.gmail.ivanjermakov1.messenger.dto.ConversationDto;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.mapper.ConversationMapper;
import com.gmail.ivanjermakov1.messenger.service.ConversationService;
import com.gmail.ivanjermakov1.messenger.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("conversation")
@Transactional
public class ConversationControllerImpl implements ConversationController {

	private final ConversationService conversationService;
	private final UserService userService;

	private ConversationMapper conversationMapper;

	@Autowired
	public ConversationControllerImpl(ConversationService conversationService, UserService userService) {
		this.conversationService = conversationService;
		this.userService = userService;
	}

	@Autowired
	public void setConversationMapper(ConversationMapper conversationMapper) {
		this.conversationMapper = conversationMapper;
	}

	@Override
	@GetMapping("create")
	public ConversationDto create(@ModelAttribute User user, @RequestParam("with") String withLogin) {
		return conversationMapper
				.with(user)
				.map(conversationService.create(user, userService.getUser(withLogin)));
	}

	@Override
	@GetMapping("delete")
	public void delete(@ModelAttribute User user, @RequestParam("id") Long id) throws AuthenticationException {
		conversationService.delete(user, conversationService.get(id));
	}

	@Override
	@GetMapping("hide")
	public void hide(@ModelAttribute User user, @RequestParam("id") Long id) {
		conversationService.hide(user, conversationService.get(id));
	}

}
<file_sep>import {Avatar} from './Avatar';

export class User {
	id: number;
	login: string;
	firstName: string;
	lastName: string;
	lastSeen: Date;
	avatar: Avatar;
}
<file_sep>package com.gmail.ivanjermakov1.messenger.controller;

import com.gmail.ivanjermakov1.messenger.dto.MessageDto;
import com.gmail.ivanjermakov1.messenger.entity.User;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface MessageController {

	/**
	 * Get list of messages.
	 *
	 * @param user           authenticated user. automatically maps, when {@literal Auth-Token} parameter present
	 * @param conversationId conversation id to get messages within
	 * @param pageable       pageable
	 * @return list of messages. Return empty list on empty conversation
	 */
	List<MessageDto> get(User user, Long conversationId, Pageable pageable);

	/**
	 * Delete list of messages.
	 * It is possible to delete only user-self messages. If in list are messages from another user they will be ignored.
	 *
	 * @param user           authenticated user. automatically maps, when {@literal Auth-Token} parameter present
	 * @param deleteMessages list of messages to delete
	 */
	void delete(User user, List<MessageDto> deleteMessages);

}
<file_sep>import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {AuthService} from '../../../service/auth.service';
import {TokenProvider} from '../../../provider/token-provider';
import {MeProvider} from '../../../provider/me-provider';
import {CookieService} from '../../../service/cookie.service';

@Component({
	selector: 'app-auth',
	templateUrl: './auth.component.html',
	styleUrls: ['./auth.component.scss']
})
export class AuthComponent implements OnInit {

	credentials = {
		login: null,
		password: <PASSWORD>
	};

	constructor(private authService: AuthService,
	            private tokenProvider: TokenProvider,
	            private meProvider: MeProvider,
	            private cookieService: CookieService,
	            private router: Router) {
	}

	ngOnInit() {
	}

	login() {
		this.authService.authenticate(this.credentials.login, this.credentials.password).subscribe(
			token => {
				this.credentials.password = <PASSWORD>;

				this.tokenProvider.setToken(token);
				// TODO: refactor so tokenProvider deal with cookies
				this.cookieService.setToken(token);

				this.authService.validate(token).subscribe(user => {
					this.meProvider.setMe(user);
					this.router.navigate(['/im'], {replaceUrl: true});
				});

			},
			error => {
				this.credentials.password = <PASSWORD>;
				return console.error(error);
			}
		);
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {API_URL} from '../../../globals';
import {EditMessage} from '../dto/EditMessage';
import {Message} from '../dto/Message';
import {NewMessage} from '../dto/NewMessage';

// TODO: deal with 2s lag between EventSource reconnections
@Injectable({
	providedIn: 'root'
})
export class MessagingService {

	constructor(private http: HttpClient) {
	}

	getEvents(token: string): Observable<any> {
		return new Observable(o => {
			const es = new EventSource(API_URL + 'messaging/listen' + '?token=' + token);
			es.addEventListener('message', (e: any) => {
				o.next(JSON.parse(e.data));
			});
		});
	}

	sendMessage(token: string, newMessage: NewMessage): Observable<Message> {
		return this.http.post<Message>(API_URL + 'messaging/send', newMessage, {
			headers: {
				'Auth-Token': token
			}
		});
	}

	editMessage(token: string, editMessage: EditMessage): Observable<Message> {
		return this.http.post<Message>(API_URL + 'messaging/edit', editMessage, {
			headers: {
				'Auth-Token': token
			}
		});
	}

}
<file_sep>import {Message} from '../Message';

export class NewMessageAction {
	message: Message;
}
<file_sep>package com.gmail.ivanjermakov1.messenger.entity;

import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import java.time.LocalDate;

/**
 * Entity representing image attached to a certain message
 */
@Entity
@Access(AccessType.FIELD)
@Table(name = "image")
public class Image {

	/**
	 * Image id
	 */
	@Id
	@Column(name = "id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	public Long id;

	/**
	 * Owner user of an image
	 */
	@ManyToOne
	@JoinColumn(name = "user_id")
	public User user;

	@ManyToOne
	@JoinColumn(name = "message_id")
	public Message message;

	/**
	 * Static resource path to an image
	 */
	@Column(name = "path")
	public String path;

	/**
	 * Date of an image upload
	 */
	@Column(name = "uploaded")
	public LocalDate uploaded;

	public Image() {
	}

	public Image(User user, Message message, String path, LocalDate uploaded) {
		this.user = user;
		this.message = message;
		this.path = path;
		this.uploaded = uploaded;
	}

}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {AppComponent} from '../../../../app.component';
import {TokenProvider} from '../../../../provider/token-provider';
import {ConversationService} from '../../../../service/conversation.service';
import {Preview} from '../../../../dto/Preview';
import {ConfirmService} from '../../../../service/confirm.service';

@Component({
	selector: 'app-conversation-menu',
	templateUrl: './conversation-menu.component.html',
	styleUrls: ['./conversation-menu.component.scss']
})
export class ConversationMenuComponent implements OnInit {

	@Input() currentPreview: Preview;
	@Output() closeConversation = new EventEmitter();

	visible = false;

	private token: string;

	constructor(
		private app: AppComponent,
		private tokenProvider: TokenProvider,
		private conversationService: ConversationService,
		private confirmService: ConfirmService
	) {
	}

	ngOnInit() {
		this.app.onLoad(() => {
			this.tokenProvider.oToken.subscribe(token => {
				this.token = token;
			});
		});
	}

	deleteConversation(conversationId: number) {
		if (this.confirmService.confirm('All conversation messages will be deleted')) {
			this.conversationService.delete(this.token, conversationId).subscribe(() => {
				this.visible = false;
				this.closeConversation.next();
			});
		}
	}

	hideConversation(conversationId: number) {
		this.conversationService.hide(this.token, conversationId).subscribe(() => {
			this.visible = false;
			this.closeConversation.next();
		});
	}

}
<file_sep>import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';

import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {FormsModule} from '@angular/forms';
import {HttpClientModule} from '@angular/common/http';
import {AutosizeModule} from 'ngx-autosize';
import {ArraySortPipeAsc, ArraySortPipeDesc} from './pipe/array-sort.pipe';
import {AuthComponent} from './component/routed/auth/auth.component';
import {RegisterComponent} from './component/routed/register/register.component';
import {ImageAttachmentComponent} from './component/embedded/attachment/image-attachment/image-attachment.component';
import {ProfileComponent} from './component/embedded/profile/profile.component';
import {ForwardedAttachmentComponent} from './component/embedded/attachment/forwarded-attachment/forwarded-attachment.component';
import {MessageComponent} from './component/embedded/message/message.component';
import {MessagingComponent} from './component/routed/messaging/messaging.component';
import {ConversationPreviewComponent} from './component/embedded/preview/conversation-preview/conversation-preview.component';
import {UserPreviewComponent} from './component/embedded/preview/user-preview/user-preview.component';
import {ProfileMenuComponent} from './component/embedded/menu/profile-menu/profile-menu.component';
import {AttachmentsMenuComponent} from './component/embedded/menu/attachments-menu/attachments-menu.component';
import {ConversationMenuComponent} from './component/embedded/menu/conversation-menu/conversation-menu.component';
import {MessageSendDirective} from './directive/message-send.directive';
import {ScrollPositionDirective} from './directive/scroll-position.directive';
import { DocumentAttachmentComponent } from './component/embedded/attachment/document-attachment/document-attachment.component';
import { ImagePreviewComponent } from './component/embedded/image-preview/image-preview.component';
import { ChatInfoComponent } from './component/embedded/chat-info/chat-info.component';

@NgModule({
	declarations: [
		AppComponent,
		AuthComponent,
		RegisterComponent,
		MessagingComponent,
		MessageComponent,
		MessageSendDirective,
		ScrollPositionDirective,
		ArraySortPipeAsc,
		ArraySortPipeDesc,
		ForwardedAttachmentComponent,
		ProfileComponent,
		ImageAttachmentComponent,
		ConversationPreviewComponent,
		UserPreviewComponent,
		ProfileMenuComponent,
		AttachmentsMenuComponent,
		ConversationMenuComponent,
		DocumentAttachmentComponent,
		ImagePreviewComponent,
		ChatInfoComponent,
	],
	imports: [
		BrowserModule,
		AppRoutingModule,
		FormsModule,
		HttpClientModule,
		AutosizeModule
	],
	providers: [],
	bootstrap: [AppComponent]
})
export class AppModule {
}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {FILE_URL} from '../../../../../../globals';
import {NewDocument} from '../../../../dto/NewDocument';

@Component({
	selector: 'app-document-attachment',
	templateUrl: './document-attachment.component.html',
	styleUrls: [
		'./document-attachment.component.scss',
		'./../attachment.scss',]
})
export class DocumentAttachmentComponent implements OnInit {

	readonly FILE_URL = FILE_URL;

	@Input() document: NewDocument;
	@Output() removeDocumentAttachment = new EventEmitter();

	constructor() {
	}

	ngOnInit() {
	}

	remove() {
		this.removeDocumentAttachment.next();
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.repository;

import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.entity.UserInfo;
import org.springframework.data.repository.CrudRepository;

import java.util.Optional;

public interface UserInfoRepository extends CrudRepository<UserInfo, Long> {

	Optional<UserInfo> findByUser(User user);

}
<file_sep>package com.gmail.ivanjermakov1.messenger.service;

import com.gmail.ivanjermakov1.messenger.dto.NewDocumentDto;
import com.gmail.ivanjermakov1.messenger.dto.enums.FileType;
import com.gmail.ivanjermakov1.messenger.entity.Document;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.exception.AuthorizationException;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import com.gmail.ivanjermakov1.messenger.repository.DocumentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

@Service
public class DocumentService {

	private final FileUploadService fileUploadService;
	private final DocumentRepository documentRepository;

	@Autowired
	public DocumentService(FileUploadService fileUploadService, DocumentRepository documentRepository) {
		this.fileUploadService = fileUploadService;
		this.documentRepository = documentRepository;
	}

	//	TODO: store original document name
	public NewDocumentDto upload(MultipartFile documentFile) throws IOException {
		return new NewDocumentDto(fileUploadService.upload(documentFile, FileType.DOCUMENT));
	}

	public void delete(User user, Long documentId) throws AuthorizationException {
		Document document = documentRepository.findById(documentId)
				.orElseThrow(() -> new NoSuchEntityException("such document does not exist"));

		if (!document.user.id.equals(user.id))
			throw new AuthorizationException("user can delete only own documents");

		documentRepository.delete(document);
	}

}
<file_sep># messenger

[![Build Status](https://img.shields.io/travis/ivanjermakov/messenger/master)](https://travis-ci.com/ivanjermakov/messenger)
[![Coverage Status](https://img.shields.io/coveralls/github/ivanjermakov/messenger/master)](https://coveralls.io/github/ivanjermakov/messenger?branch=master)
[![GitHub release](https://img.shields.io/github/release/ivanjermakov/messenger.svg)](https://github.com/ivanjermakov/messenger/releases)
[![Hits-of-Code](https://hitsofcode.com/github/ivanjermakov/messenger)](https://hitsofcode.com/view/github/ivanjermakov/messenger)

Just a messenger.
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {API_URL} from '../../../globals';
import {Preview} from '../dto/Preview';
import {User} from '../dto/User';

@Injectable({
	providedIn: 'root'
})
export class SearchService {

	constructor(private http: HttpClient) {
	}

	searchConversations(token: string, search: string): Observable<Preview[]> {
		return this.http.get<Preview[]>(API_URL + 'search/conversations', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'search': search
			}
		});
	}

	searchUsers(token: string, search: string): Observable<User[]> {
		return this.http.get<User[]>(API_URL + 'search/users', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'search': search
			}
		});
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.test.integration;

import com.gmail.ivanjermakov1.messenger.controller.ConversationController;
import com.gmail.ivanjermakov1.messenger.controller.MessagingController;
import com.gmail.ivanjermakov1.messenger.controller.SearchController;
import com.gmail.ivanjermakov1.messenger.dto.ConversationDto;
import com.gmail.ivanjermakov1.messenger.dto.NewMessageDto;
import com.gmail.ivanjermakov1.messenger.dto.PreviewDto;
import com.gmail.ivanjermakov1.messenger.dto.TestingUser;
import com.gmail.ivanjermakov1.messenger.dto.UserDto;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.InvalidEntityException;
import com.gmail.ivanjermakov1.messenger.exception.InvalidSearchFormatException;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import com.gmail.ivanjermakov1.messenger.exception.RegistrationException;
import com.gmail.ivanjermakov1.messenger.service.TestingService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class SearchTest {

	@Autowired
	private SearchController searchController;

	@Autowired
	private ConversationController conversationController;

	@Autowired
	private MessagingController messagingController;

	@Autowired
	private TestingService testingService;

	@Test
	public void shouldFindUserByLogin() throws RegistrationException, AuthenticationException, InvalidSearchFormatException {
		TestingUser user = testingService.registerUser("John");

		List<UserDto> searchResult = searchController.searchUsers(
				user.user,
				"@John",
				PageRequest.of(0, Integer.MAX_VALUE)
		);

		Assert.assertTrue(searchResult
				.stream()
				.anyMatch(dto -> dto.login.equals(user.user.login))
		);
	}

	@Test(expected = InvalidSearchFormatException.class)
	public void shouldThrowException_WithInvalidUserSearch() throws RegistrationException, AuthenticationException, InvalidSearchFormatException {
		TestingUser user = testingService.registerUser("John");

		searchController.searchUsers(
				user.user,
				"John",
				PageRequest.of(0, Integer.MAX_VALUE)
		);
	}

	@Test(expected = InvalidEntityException.class)
	public void shouldFindConversation() throws RegistrationException, AuthenticationException {
		TestingUser user1 = testingService.registerUser("Jack");
		TestingUser user2 = testingService.registerUser("Ron");

		ConversationDto conversationDto = conversationController.create(
				user1.user,
				user2.user.login
		);

		NewMessageDto message = new NewMessageDto(
				user1.user.id,
				conversationDto.id,
				"Hello!"
		);

//		at least one message is required for conversation to be visible
		messagingController.sendMessage(user1.user, message);

		List<PreviewDto> previews = searchController.searchConversations(
				user1.user,
				user2.userDto.lastName,
				PageRequest.of(0, Integer.MAX_VALUE)
		);

		Assert.assertEquals(1, previews.size());
		Assert.assertEquals(
				conversationDto.id,
				previews
						.stream()
						.findFirst()
						.orElseThrow(NoSuchEntityException::new)
						.conversation.id
		);

		Assert.assertEquals(
				1,
				searchController
						.searchConversations(
								user1.user,
								"ron",
								PageRequest.of(0, Integer.MAX_VALUE)
						)
						.size()
		);

		Assert.assertEquals(
				1,
				searchController
						.searchConversations(
								user1.user,
								"ro",
								PageRequest.of(0, Integer.MAX_VALUE)
						)
						.size()
		);

		Assert.assertEquals(
				1,
				searchController
						.searchConversations(
								user1.user,
								"ron",
								PageRequest.of(0, Integer.MAX_VALUE)
						)
						.size()
		);

		Assert.assertEquals(
				0,
				searchController
						.searchConversations(
								user1.user,
								"abcdef",
								PageRequest.of(0, Integer.MAX_VALUE)
						)
						.size()
		);

		searchController
				.searchConversations(
						user1.user,
						"",
						PageRequest.of(0, Integer.MAX_VALUE)
				);
	}

}
<file_sep>import {Component, EventEmitter, HostListener, Input, OnInit, Output} from '@angular/core';
import {Image} from '../../../dto/Image';
import {MessageImage} from '../../../dto/local/MessageImage';
import {ImageCompressionMode} from '../../../dto/enum/ImageCompressionMode';
import {ImageService} from '../../../service/image.service';
import {FILE_URL} from '../../../../../globals';
import * as moment from 'moment';
import {ConfirmService} from '../../../service/confirm.service';
import {MeProvider} from '../../../provider/me-provider';
import {AppComponent} from '../../../app.component';
import {User} from '../../../dto/User';

@Component({
	selector: 'app-image-preview',
	templateUrl: './image-preview.component.html',
	styleUrls: ['./image-preview.component.scss']
})
export class ImagePreviewComponent implements OnInit {

	readonly ImageCompressionMode: typeof ImageCompressionMode = ImageCompressionMode;
	readonly ImageService: typeof ImageService = ImageService;
	readonly FILE_URL = FILE_URL;

	@Input()
	messageImage: MessageImage;

	@Output()
	deleteImageEvent = new EventEmitter();

	images: Image[];
	currentImageIndex: number;
	me: User;

	@HostListener('document:keydown', ['$event'])
	handleKeyboardEvent(event: KeyboardEvent) {
		if (event.code === 'ArrowLeft') {
			this.openPrevious();
			return;
		}
		if (event.code === 'ArrowRight') {
			this.openNext();
			return;
		}
	}

	constructor(
		private app: AppComponent,
		private meProvider: MeProvider,
		private confirmService: ConfirmService,
	) {
	}

	ngOnInit() {
		this.app.onLoad(() => {
			this.meProvider.oMe.subscribe(me => {
				this.me = me;
			});
		});
		this.images = this.messageImage.message.images;
		this.currentImageIndex = this.images.findIndex(i => i.id === this.messageImage.image.id);
	}

	openFullImage() {
		window.open(FILE_URL + this.images[this.currentImageIndex].path, '_blank');
	}

	formatTime(date: Date) {
		return moment(date).format('hh:mm');
	}

	deleteImage() {
		if (this.confirmService.confirm('This image will be deleted')) {
			this.deleteImageEvent.emit();
		}
	}

	openPrevious() {
		if (this.currentImageIndex !== 0) {
			this.currentImageIndex--;
		}
	}

	openNext() {
		if (this.currentImageIndex !== this.images.length - 1) {
			this.currentImageIndex++;
		}
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {Observable} from 'rxjs';
import {API_URL} from '../../../globals';
import {Message} from '../dto/Message';
import {Pageable} from '../dto/Pageable';

@Injectable({
	providedIn: 'root'
})
export class MessageService {

	constructor(private http: HttpClient) {
	}

	get(token: string, conversationId: number, pageable: Pageable): Observable<Message[]> {
		return this.http.get<Message[]>(API_URL + 'message/get', {
			headers: {
				'Auth-Token': token
			},
			params: pageable.toHttpParams()
				.append('conversationId', conversationId.toString())
		});
	}

	delete(token: string, deleteMessages: Message[]) {
		return this.http.post(API_URL + 'message/delete', deleteMessages, {
			headers: {
				'Auth-Token': token
			}
		});
	}

}
<file_sep>export class Avatar {

	id: number;
	path: string;
	uploaded: Date;

}
<file_sep>import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {AuthComponent} from './component/routed/auth/auth.component';
import {RegisterComponent} from './component/routed/register/register.component';
import {MessagingComponent} from './component/routed/messaging/messaging.component';

const routes: Routes = [
	{
		path: 'auth',
		component: AuthComponent
	},
	{
		path: 'register',
		component: RegisterComponent
	},
	{
		path: 'im',
		component: MessagingComponent
	}
];

@NgModule({
	imports: [RouterModule.forRoot(routes)],
	exports: [RouterModule]
})
export class AppRoutingModule {
}
<file_sep>export class RegisterUser {
	firstName: string;
	lastName: string;
	login: string;
	password: string;
}
<file_sep>package com.gmail.ivanjermakov1.messenger.service;

import com.gmail.ivanjermakov1.messenger.dto.PreviewDto;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.mapper.ConversationMapper;
import com.gmail.ivanjermakov1.messenger.mapper.PreviewMapper;
import com.gmail.ivanjermakov1.messenger.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class PreviewService {

	private final ConversationService conversationService;

	private final PreviewMapper previewMapper;

	@Autowired
	public PreviewService(ConversationService conversationService, UserMapper userMapper, MessageService messageService, PreviewMapper previewMapper) {
		this.conversationService = conversationService;
		this.previewMapper = previewMapper;
	}

	@Autowired
	public void setConversationMapper(ConversationMapper conversationMapper) {
	}

	public List<PreviewDto> all(User user, Pageable pageable) {
		return conversationService.getConversations(user, PageRequest.of(0, Integer.MAX_VALUE))
				.stream()
				.map(c -> previewMapper.with(user).map(c))
				.filter(p -> p.lastMessage != null)
				.sorted(Comparator.comparing(p -> p.lastMessage.sent, Comparator.reverseOrder()))
				.skip(pageable.getOffset())
				.limit(pageable.getPageSize())
				.collect(Collectors.toList());
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.repository;

import com.gmail.ivanjermakov1.messenger.entity.Conversation;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

import java.util.List;

public interface ConversationRepository extends CrudRepository<Conversation, Long> {

	@Query("select c from Conversation c join c.userConversations uc join uc.user u on u.id = :id")
	List<Conversation> getConversations(@Param("id") Long userId, Pageable pageable);

	/**
	 * TODO: docs
	 *
	 * @param userId
	 * @param search
	 * @return
	 */
	@Query(value = "select c.id, c.chat_name, c.creator_id\n" +
			"from conversation c\n" +
			"         join user_conversation uc on uc.conversation_id = c.id\n" +
			"         join \"user\" u on u.id = uc.user_id\n" +
			"         join user_info ui on ui.user_id = u.id\n" +
			"where u.id <> :id\n" +
			"  and (\n" +
			"        lower(u.login) like '%' || lower(:s) || '%' or\n" +
			"        lower(ui.first_name) like '%' || lower(:s) || '%' or\n" +
			"        lower(ui.last_name) like '%' || lower(:s) || '%'\n" +
			"    )", nativeQuery = true)
	List<Conversation> findConversationsBySearchQuery(@Param("id") Long userId, @Param("s") String search);

}
<file_sep>package com.gmail.ivanjermakov1.messenger.controller;

import com.gmail.ivanjermakov1.messenger.dto.MessageDto;
import com.gmail.ivanjermakov1.messenger.entity.User;
import com.gmail.ivanjermakov1.messenger.service.MessageService;
import com.gmail.ivanjermakov1.messenger.service.MessagingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("message")
@Transactional
public class MessageControllerImpl implements MessageController {

	private final MessageService messageService;
	private final MessagingService messagingService;

	@Autowired
	public MessageControllerImpl(MessageService messageService, MessagingService messagingService) {
		this.messageService = messageService;
		this.messagingService = messagingService;
	}

	@Override
	@GetMapping("get")
	public List<MessageDto> get(@ModelAttribute User user,
	                            @RequestParam("conversationId") Long conversationId,
	                            @PageableDefault(direction = Sort.Direction.DESC, sort = {"sent"}) Pageable pageable) {
		messagingService.processConversationRead(user, conversationId);
		return messageService.get(user.id, conversationId, pageable);
	}

	@Override
	@PostMapping("delete")
	public void delete(@ModelAttribute User user,
	                   @RequestBody List<MessageDto> deleteMessages) {
		messageService.delete(user, deleteMessages);
	}

}
<file_sep>package com.gmail.ivanjermakov1.messenger.service;

import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import org.springframework.stereotype.Service;

@Service
public class MarkdownService {

	private final Parser parser = Parser.builder().build();
	private final HtmlRenderer renderer = HtmlRenderer.builder().build();

	public String format(String plain) {
		return renderer.render(parser.parse(plain)).trim();
	}

}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {User} from '../../../../dto/User';
import {AppComponent} from '../../../../app.component';
import {MeProvider} from '../../../../provider/me-provider';
import {ImageCompressionMode} from '../../../../dto/enum/ImageCompressionMode';
import {ImageService} from '../../../../service/image.service';
import {FILE_URL} from '../../../../../../globals';
import {Preview} from '../../../../dto/Preview';
import {CookieService} from '../../../../service/cookie.service';
import {Router} from '@angular/router';

@Component({
	selector: 'app-profile-menu',
	templateUrl: './profile-menu.component.html',
	styleUrls: ['./profile-menu.component.scss']
})
export class ProfileMenuComponent implements OnInit {

	readonly ImageCompressionMode: typeof ImageCompressionMode = ImageCompressionMode;
	readonly ImageService: typeof ImageService = ImageService;
	readonly FILE_URL = FILE_URL;

	@Input() currentPreview: Preview;
	@Output() openProfile = new EventEmitter<User>();

	visible = false;
	me: User;

	constructor(
		private app: AppComponent,
		private meProvider: MeProvider,
		private cookieService: CookieService,
		private router: Router,
	) {
	}

	ngOnInit() {
		this.app.onLoad(() => {
			this.meProvider.oMe.subscribe(me => {
				this.me = me;
			});
		});
	}

	logout() {
		this.cookieService.deleteToken();
		location.replace('/');
	}

	open(me: User) {
		this.visible = false;
		this.openProfile.next(me);
	}
}
<file_sep>import {Injectable} from '@angular/core';

@Injectable({
	providedIn: 'root'
})
export class CookieService {

	LOCAL_STORAGE_TOKEN_NAME = 'Auth-Token';

	constructor() {
	}

	getToken(): string {
		return localStorage.getItem(this.LOCAL_STORAGE_TOKEN_NAME);
	}

	setToken(token: string) {
		localStorage.setItem(this.LOCAL_STORAGE_TOKEN_NAME, token);
	}

	deleteToken() {
		localStorage.removeItem(this.LOCAL_STORAGE_TOKEN_NAME);
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {API_URL} from '../../../globals';
import {Observable} from 'rxjs';
import {Conversation} from '../dto/Conversation';

@Injectable({
	providedIn: 'root'
})
export class ConversationService {

	constructor(private http: HttpClient) {
	}

	create(token: string, withLogin: string): Observable<Conversation> {
		return this.http.get<Conversation>(API_URL + 'conversation/create', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'with': withLogin
			}
		});
	}

	delete(token: string, conversationId: number) {
		return this.http.get(API_URL + 'conversation/delete', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'id': conversationId.toString()
			}
		});
	}

	hide(token: string, conversationId: number) {
		return this.http.get(API_URL + 'conversation/hide', {
			headers: {
				'Auth-Token': token
			},
			params: {
				'id': conversationId.toString()
			}
		});
	}

}
<file_sep>import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';

import * as moment from 'moment';
import {FILE_URL} from '../../../../../globals';
import {User} from '../../../dto/User';
import {UserInfo} from '../../../dto/UserInfo';
import {MaritalStatus} from '../../../dto/enum/MaritalStatus';

@Component({
	selector: 'app-profile',
	templateUrl: './profile.component.html',
	styleUrls: [
		'./profile.component.scss',
		'./../../routed/messaging/style/messaging.component.search.scss'
	]
})
export class ProfileComponent implements OnInit {

	readonly FILE_URL = FILE_URL;

	@Input() currentProfile;
	@Input() me: User;
	@Output() closeProfile = new EventEmitter();
	@Output() editProfile = new EventEmitter<UserInfo>();

	@ViewChild('fileInput') fileInput;

	unloadedAvatar = {
		path: null
	};

	maritalStatuses = Object.keys(MaritalStatus).filter(key => typeof MaritalStatus[key] === 'number');

	editable = false;

	editView = false;

	date = {
		days: Array.from({length: 31}, (x, i) => i + 1),
		months: Array.from({length: 12}, (x, i) => i + 1),
		years: Array.from({length: 100}, (x, i) => moment().year() - i),
	};

	selectedDate = {
		day: null,
		month: null,
		year: null
	};

	constructor() {
	}

	ngOnInit() {
		this.editable = this.me.login === this.currentProfile.user.login;

		if (this.currentProfile.userInfo.birthDate) {
			const dme = this.dme(this.currentProfile.userInfo.birthDate);

			this.selectedDate.year = dme[0];
			this.selectedDate.month = dme[1];
			this.selectedDate.day = dme[2];
		}
	}

	close() {
		this.unloadedAvatar.path = null;
		this.closeProfile.emit();
	}

	edit() {
		this.currentProfile.userInfo.maritalStatus = this.currentProfile.userInfo.maritalStatus !== 'null' ?
			this.currentProfile.userInfo.maritalStatus : null;
		if (this.selectedDate.day && this.selectedDate.month && this.selectedDate.year) {
			this.currentProfile.userInfo.birthDate = moment([
				parseInt(this.selectedDate.year, 10),
				// TODO: investigate month number shifting
				parseInt(this.monthNumber(this.selectedDate.month), 10) - 2,
				parseInt(this.selectedDate.day, 10)
			]).format('YYYY-MM-DD');
		} else {
			this.currentProfile.userInfo.birthDate = null;
		}
		if (this.fileInput.nativeElement.files[0]) {
			this.currentProfile.userInfo.newAvatar = this.fileInput.nativeElement.files[0];
		}
		this.editProfile.emit(this.currentProfile.userInfo);
		this.editView = false;
	}

	monthName(n: number) {
		return moment().month(n - 1).format('MMMM');
	}

	monthNumber(monthName: string) {
		return moment().month(monthName).format('M');
	}

	dme(date) {
		const d = moment(date).toArray().slice(0, 3);
		d[1] = d[1] + 1;
		return d;
	}

	selectAvatar() {
		this.fileInput.nativeElement.click();
	}

	onAvatarSelect() {
		const reader = new FileReader();
		reader.onload = (e: any) => {
			this.unloadedAvatar.path = e.target.result;
		};
		reader.readAsDataURL(this.fileInput.nativeElement.files[0]);
	}

}
<file_sep>import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {NewImage} from '../dto/NewImage';
import {API_URL} from '../../../globals';
import {HttpClient} from '@angular/common/http';

@Injectable({
	providedIn: 'root'
})
export class DocumentService {

	constructor(private http: HttpClient) {
	}

	upload(token: string, file): Observable<NewImage> {
		const formData = new FormData();
		formData.append('document', file);

		return this.http.post<NewImage>(API_URL + 'document/upload', formData, {
			headers: {
				'Auth-Token': token
			}
		});
	}

}
<file_sep>export class NewChat {
	name: string;
	memberIds: number[];
}
<file_sep>export enum ImageCompressionMode {
	FULL = 'f',
	MEDIUM = 'm',
	SMALL = 's'
}
<file_sep>package com.gmail.ivanjermakov1.messenger.test.unit;

import com.gmail.ivanjermakov1.messenger.dto.TestingUser;
import com.gmail.ivanjermakov1.messenger.dto.UserInfoDto;
import com.gmail.ivanjermakov1.messenger.exception.AuthenticationException;
import com.gmail.ivanjermakov1.messenger.exception.NoSuchEntityException;
import com.gmail.ivanjermakov1.messenger.exception.RegistrationException;
import com.gmail.ivanjermakov1.messenger.repository.UserInfoRepository;
import com.gmail.ivanjermakov1.messenger.service.TestingService;
import com.gmail.ivanjermakov1.messenger.service.UserInfoService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.when;

@SpringBootTest
@RunWith(SpringRunner.class)
@Transactional
public class UserInfoUnitTest {

	@InjectMocks
	private UserInfoService userInfoService;

	@Autowired
	private TestingService testingService;

	@Mock
	private UserInfoRepository userInfoRepository;

	private TestingUser user;

	@Before
	public void before() throws RegistrationException, AuthenticationException {
		user = testingService.registerUser("Jack");
	}

	@Test(expected = NoSuchEntityException.class)
	public void shouldThrowException_WhenNoSuchUserInfo() {
		when(userInfoRepository.findByUser(any()))
				.thenReturn(Optional.empty());

		userInfoService.edit(
				user.user,
				new UserInfoDto(
						user.userDto,
						"Jack",
						"Jack"
				)
		);
	}

}
