From 58e3f4a34cff2e9c5f270d162e848d3d0dedbed4 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Mon, 29 Jul 2019 21:05:37 -0400 Subject: [PATCH 1/3] added custom emoji support --- .../emoji-picker/emoji-picker.component.html | 11 ++--- .../emoji-picker/emoji-picker.component.ts | 47 +++++++++++++++++-- src/app/services/mastodon.service.ts | 9 +++- src/app/services/models/api.settings.ts | 1 + src/app/services/tools.service.ts | 13 ++++- 5 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/app/components/create-status/emoji-picker/emoji-picker.component.html b/src/app/components/create-status/emoji-picker/emoji-picker.component.html index b31c1c73..4b09bd42 100644 --- a/src/app/components/create-status/emoji-picker/emoji-picker.component.html +++ b/src/app/components/create-status/emoji-picker/emoji-picker.component.html @@ -1,8 +1,5 @@ \ No newline at end of file + *ngIf="loaded" + [showPreview]="false" [perLine]="7" [isNative]="true" [sheetSize]="16" [emojiTooltip]="true" + [custom]="customEmojis" (emojiSelect)="emojiSelected($event)" class="emojipicker" title="Pick your emoji…" + emoji="point_up"> \ No newline at end of file diff --git a/src/app/components/create-status/emoji-picker/emoji-picker.component.ts b/src/app/components/create-status/emoji-picker/emoji-picker.component.ts index f4cdd34c..3da1aaf8 100644 --- a/src/app/components/create-status/emoji-picker/emoji-picker.component.ts +++ b/src/app/components/create-status/emoji-picker/emoji-picker.component.ts @@ -1,5 +1,9 @@ import { Component, OnInit, HostListener, ElementRef, Output, EventEmitter } from '@angular/core'; +import { ToolsService } from '../../../services/tools.service'; +import { NotificationService } from '../../../services/notification.service'; +import { Emoji } from '../../../services/models/mastodon.interfaces'; + @Component({ selector: 'app-emoji-picker', templateUrl: './emoji-picker.component.html', @@ -11,25 +15,62 @@ export class EmojiPickerComponent implements OnInit { @Output('closed') public closedEvent = new EventEmitter(); @Output('emojiSelected') public emojiSelectedEvent = new EventEmitter(); - constructor(private eRef: ElementRef) { } + customEmojis: PickerCustomEmoji[] = []; + loaded: boolean; + + constructor( + private notificationService: NotificationService, + private toolsService: ToolsService, + private eRef: ElementRef) { } @HostListener('document:click', ['$event']) clickout(event) { if (!this.init) return; - if (!this.eRef.nativeElement.contains(event.target)) { + if (!this.eRef.nativeElement.contains(event.target)) { this.closedEvent.emit(null); } } ngOnInit() { + let currentAccount = this.toolsService.getSelectedAccounts()[0]; + this.toolsService.getCustomEmojis(currentAccount) + .then(emojis => { + console.warn(emojis); + this.customEmojis = emojis.map(x => this.convertEmoji(x)); + }) + .catch(err => { + this.notificationService.notifyHttpError(err); + }) + .then(() => { + this.loaded = true; + }); + setTimeout(() => { this.init = true; }, 0); } + private convertEmoji(emoji: Emoji): PickerCustomEmoji { + return new PickerCustomEmoji(emoji.shortcode, [emoji.shortcode], emoji.shortcode, [emoji.shortcode], emoji.url); + } + emojiSelected(select: any): boolean { - this.emojiSelectedEvent.next(select.emoji.native); + if (select.emoji.custom) { + this.emojiSelectedEvent.next(select.emoji.colons); + } else { + this.emojiSelectedEvent.next(select.emoji.native); + } return false; } } + +class PickerCustomEmoji { + constructor( + public name: string, + public shortNames: string[], + public text: string, + public keywords: string[], + public imageUrl: string) { + } +} \ No newline at end of file diff --git a/src/app/services/mastodon.service.ts b/src/app/services/mastodon.service.ts index cf7b5450..a7ef67e2 100644 --- a/src/app/services/mastodon.service.ts +++ b/src/app/services/mastodon.service.ts @@ -2,12 +2,12 @@ import { Injectable } from '@angular/core'; import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http'; import { ApiRoutes } from './models/api.settings'; -import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll } from "./models/mastodon.interfaces"; +import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification, List, Poll, Emoji } from "./models/mastodon.interfaces"; import { AccountInfo } from '../states/accounts.state'; import { StreamTypeEnum, StreamElement } from '../states/streams.state'; @Injectable() -export class MastodonService { +export class MastodonService { private apiRoutes = new ApiRoutes(); constructor(private readonly httpClient: HttpClient) { } @@ -367,6 +367,11 @@ export class MastodonService { let route = `https://${account.instance}${this.apiRoutes.deleteStatus}`.replace('{0}', statusId.toString()); const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` }); return this.httpClient.delete(route, { headers: headers }).toPromise(); + } + + getCustomEmojis(account: AccountInfo): Promise { + let route = `https://${account.instance}${this.apiRoutes.getCustomEmojis}`; + return this.httpClient.get(route).toPromise(); } } diff --git a/src/app/services/models/api.settings.ts b/src/app/services/models/api.settings.ts index f39470f2..6de33ba5 100644 --- a/src/app/services/models/api.settings.ts +++ b/src/app/services/models/api.settings.ts @@ -3,6 +3,7 @@ export class ApiRoutes { createApp = '/api/v1/apps'; getToken = '/oauth/token'; getAccount = '/api/v1/accounts/{0}'; + getCustomEmojis = '/api/v1/custom_emojis'; getCurrentAccount = '/api/v1/accounts/verify_credentials'; getAccountFollowers = '/api/v1/accounts/{0}/followers'; getAccountFollowing = '/api/v1/accounts/{0}/following'; diff --git a/src/app/services/tools.service.ts b/src/app/services/tools.service.ts index ca65d320..01b84cd1 100644 --- a/src/app/services/tools.service.ts +++ b/src/app/services/tools.service.ts @@ -3,14 +3,14 @@ import { Store } from '@ngxs/store'; import { AccountInfo } from '../states/accounts.state'; import { MastodonService } from './mastodon.service'; -import { Account, Results, Status } from "./models/mastodon.interfaces"; +import { Account, Results, Status, Emoji } from "./models/mastodon.interfaces"; import { StatusWrapper } from '../models/common.model'; import { AccountSettings, SaveAccountSettings } from '../states/settings.state'; @Injectable({ providedIn: 'root' }) -export class ToolsService { +export class ToolsService { constructor( private readonly mastodonService: MastodonService, private readonly store: Store) { } @@ -82,6 +82,15 @@ export class ToolsService { } return `@${fullHandle}`; } + + getCustomEmojis(account: AccountInfo): Promise { + //TODO: add cache + + return this.mastodonService.getCustomEmojis(account) + .then(emojis => { + return emojis.filter(x => x.visible_in_picker); + }); + } } export class OpenThreadEvent { From 761a910d3138a11c65809d6c39a1bc0a756b8cb5 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Mon, 29 Jul 2019 21:10:26 -0400 Subject: [PATCH 2/3] fix custom emoji positionning --- .../create-status/emoji-picker/emoji-picker.component.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/app/components/create-status/emoji-picker/emoji-picker.component.scss b/src/app/components/create-status/emoji-picker/emoji-picker.component.scss index d9c7b5c0..0c3d8770 100644 --- a/src/app/components/create-status/emoji-picker/emoji-picker.component.scss +++ b/src/app/components/create-status/emoji-picker/emoji-picker.component.scss @@ -13,4 +13,10 @@ left: -1px; font-size: 19px !important; cursor: pointer !important; +} + +::ng-deep .emoji-mart-emoji-custom span { + position: relative; + top: 0px; + left: 0px; } \ No newline at end of file From f11d82c072df90fe93219644938458d5f5bd804a Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Mon, 29 Jul 2019 21:20:12 -0400 Subject: [PATCH 3/3] added emoji cache --- src/app/services/tools.service.ts | 44 +++++++++++++++++-------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/app/services/tools.service.ts b/src/app/services/tools.service.ts index 01b84cd1..be46472c 100644 --- a/src/app/services/tools.service.ts +++ b/src/app/services/tools.service.ts @@ -10,7 +10,7 @@ import { AccountSettings, SaveAccountSettings } from '../states/settings.state'; @Injectable({ providedIn: 'root' }) -export class ToolsService { +export class ToolsService { constructor( private readonly mastodonService: MastodonService, private readonly store: Store) { } @@ -20,23 +20,23 @@ export class ToolsService { var regAccounts = this.store.snapshot().registeredaccounts.accounts; return regAccounts.filter(x => x.isSelected); } - + getAccountSettings(account: AccountInfo): AccountSettings { var accountsSettings = this.store.snapshot().globalsettings.settings.accountSettings; let accountSettings = accountsSettings.find(x => x.accountId === account.id); - if(!accountSettings){ + if (!accountSettings) { accountSettings = new AccountSettings(); accountSettings.accountId = account.id; - this.saveAccountSettings(accountSettings); + this.saveAccountSettings(accountSettings); } - if(!accountSettings.customStatusCharLength){ + if (!accountSettings.customStatusCharLength) { accountSettings.customStatusCharLength = 500; this.saveAccountSettings(accountSettings); } return accountSettings; } - saveAccountSettings(accountSettings: AccountSettings){ + saveAccountSettings(accountSettings: AccountSettings) { this.store.dispatch([ new SaveAccountSettings(accountSettings) ]) @@ -45,19 +45,19 @@ export class ToolsService { findAccount(account: AccountInfo, accountName: string): Promise { return this.mastodonService.search(account, accountName, true) .then((result: Results) => { - if(accountName[0] === '@') accountName = accountName.substr(1); + if (accountName[0] === '@') accountName = accountName.substr(1); const foundAccount = result.accounts.find( x => (x.acct.toLowerCase() === accountName.toLowerCase() - || - (x.acct.toLowerCase().split('@')[0] === accountName.toLowerCase().split('@')[0]) - && x.url.replace('https://', '').split('/')[0] === accountName.toLowerCase().split('@')[1]) - ); + || + (x.acct.toLowerCase().split('@')[0] === accountName.toLowerCase().split('@')[0]) + && x.url.replace('https://', '').split('/')[0] === accountName.toLowerCase().split('@')[1]) + ); return foundAccount; }); } - getStatusUsableByAccount(account: AccountInfo, originalStatus: StatusWrapper): Promise{ + getStatusUsableByAccount(account: AccountInfo, originalStatus: StatusWrapper): Promise { const isProvider = originalStatus.provider.id === account.id; let statusPromise: Promise = Promise.resolve(originalStatus.status); @@ -82,15 +82,19 @@ export class ToolsService { } return `@${fullHandle}`; } - - getCustomEmojis(account: AccountInfo): Promise { - //TODO: add cache - return this.mastodonService.getCustomEmojis(account) - .then(emojis => { - return emojis.filter(x => x.visible_in_picker); - }); - } + private emojiCache: { [id: string]: Emoji[] } = {}; + getCustomEmojis(account: AccountInfo): Promise { + if (this.emojiCache[account.id]) { + return Promise.resolve(this.emojiCache[account.id]); + } else { + return this.mastodonService.getCustomEmojis(account) + .then(emojis => { + this.emojiCache[account.id] = emojis.filter(x => x.visible_in_picker); + return this.emojiCache[account.id]; + }); + } + } } export class OpenThreadEvent {