diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..be21fc1f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "semi": true, + "trailingComma": "all", + "printWidth": 140, + "tabWidth": 4, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "auto" +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 3aa13fb6..40db2077 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.validate": ["typescript", "vue", "html"] } diff --git a/app/api/SkyChatClient.ts b/app/api/SkyChatClient.ts index fe37fcc9..90dd70fa 100644 --- a/app/api/SkyChatClient.ts +++ b/app/api/SkyChatClient.ts @@ -36,6 +36,7 @@ const defaultUser: SanitizedUser = { export declare interface SkyChatClient { on(event: 'config', listener: (config: PublicConfig) => any): this; + on(event: 'sticker-list', listener: (stickers: Record) => any): this; on(event: 'custom', listener: (custom: CustomizationElements) => any): this; on(event: 'set-user', listener: (user: SanitizedUser) => any): this; on(event: 'auth-token', listener: (token: AuthToken | null) => any): this; @@ -78,6 +79,7 @@ export class SkyChatClient extends EventEmitter { private _user: SanitizedUser = defaultUser; private _config: PublicConfig | null = null; + private _stickers: Record = {}; private _custom: CustomizationElements = {}; private _token: AuthToken | null = null; private _connectedList: Array = []; @@ -113,6 +115,7 @@ export class SkyChatClient extends EventEmitter { // Auth & Config this.on('config', this._onConfig.bind(this)); + this.on('sticker-list', this._onStickerList.bind(this)); this.on('custom', this._onCustom.bind(this)); this.on('set-user', this._onUser.bind(this)); this.on('auth-token', this._onToken.bind(this)); @@ -159,6 +162,11 @@ export class SkyChatClient extends EventEmitter { this.emit('update', this.state); } + private _onStickerList(stickers: Record) { + this._stickers = stickers; + this.emit('update', this.state); + } + private _onCustom(custom: CustomizationElements) { this._custom = custom; this.emit('update', this.state); @@ -379,6 +387,7 @@ export class SkyChatClient extends EventEmitter { websocketReadyState: this._websocket ? this._websocket.readyState : WebSocket.CLOSED, user: this._user, config: this._config, + stickers: this._stickers, custom: this._custom, token: this._token, connectedList: this._connectedList, diff --git a/app/client/src/components/message/MessagePannel.vue b/app/client/src/components/message/MessagePannel.vue index cdb94338..799759cd 100644 --- a/app/client/src/components/message/MessagePannel.vue +++ b/app/client/src/components/message/MessagePannel.vue @@ -1,10 +1,8 @@ diff --git a/app/client/src/components/message/NewMessageForm.vue b/app/client/src/components/message/NewMessageForm.vue index 0ad382d8..8f82c001 100644 --- a/app/client/src/components/message/NewMessageForm.vue +++ b/app/client/src/components/message/NewMessageForm.vue @@ -4,6 +4,7 @@ import { useAppStore } from '@/stores/app'; import { useClientStore } from '@/stores/client'; import { AudioRecorder } from '@/lib/AudioRecorder'; import { RisiBank } from 'risibank-web-api'; +import { Mentionable } from 'vue-mention'; const MESSAGE_HISTORY_LENGTH = 500; @@ -150,22 +151,6 @@ const openRisiBank = function () { }); }; -/** - * Autocomplete the username - */ -const onKeyUpTab = function () { - const messageMatch = app.newMessage.match(/([*a-zA-Z0-9_-]+)$/); - const username = messageMatch ? messageMatch[0].toLowerCase() : null; - if (!username) { - return; - } - const matches = client.state.connectedList.map((entry) => entry.identifier).filter((identifier) => identifier.indexOf(username) === 0); - if (matches.length !== 1) { - return; - } - app.setMessage(app.newMessage.substr(0, app.newMessage.length - username.length) + matches[0]); -}; - /** * When the file input changed */ @@ -212,9 +197,44 @@ const cancelAudio = function () { } recordingAudio.value = false; }; + +const autoSuggestItems = ref([]); +const autoSuggestOpen = ref(false); +function onOpen(key) { + autoSuggestOpen.value = true; + if (key === '#') { + autoSuggestItems.value = Object.values(client.state.rooms).map((room) => ({ + value: room.name, + searchText: room.name, + label: room.name, + })); + } else if (key === '@') { + autoSuggestItems.value = Object.values(client.state.connectedList).map((user) => ({ + value: user.identifier, + searchText: user.identifier, + label: user.identifier, + })); + } else if (key === ':') { + autoSuggestItems.value = Object.entries(client.state.stickers).map(([name, sticker]) => ({ + value: name.substring(1), + searchText: name, + label: name, + url: sticker, + })); + } else { + autoSuggestItems.value = []; + } +} + +function onClose() { + autoSuggestOpen.value = false; + autoSuggestItems.value = []; +} - + diff --git a/app/client/src/components/message/SingleMessage.vue b/app/client/src/components/message/SingleMessage.vue index 29628eb7..3287708e 100644 --- a/app/client/src/components/message/SingleMessage.vue +++ b/app/client/src/components/message/SingleMessage.vue @@ -30,6 +30,10 @@ const props = defineProps({ type: Boolean, default: false, }, + showDate: { + type: Boolean, + default: false, + }, }); const content = ref(null); @@ -42,8 +46,29 @@ const isBlacklisted = computed(() => { return blacklist.includes(props.message.user.username.toLowerCase()); }); -// Shown date +// Shown dates const formattedDate = computed(() => { + // Show "today" if today + // Show "yesterday" if yesterday + // Show "Month day" if this year + // Show "Month day, year" if not this year + const date = new Date(props.message.createdTimestamp * 1000); + const today = new Date(); + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + if (date.toDateString() === today.toDateString()) { + return 'Today'; + } + if (date.toDateString() === yesterday.toDateString()) { + return 'Yesterday'; + } + if (date.getFullYear() === today.getFullYear()) { + return date.toLocaleString('default', { month: 'long', day: 'numeric' }); + } + return date.toLocaleString('default', { month: 'long', day: 'numeric', year: 'numeric' }); +}); +const formattedTime = computed(() => { + // Show time as "HH:MM:SS" const date = new Date(props.message.createdTimestamp * 1000); const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); @@ -85,7 +110,11 @@ const bindMessageContentEvents = () => { const buttons = Array.from(content.value.getElementsByClassName('skychat-button')); for (const button of buttons) { button.addEventListener('click', () => { - if (button.dataset.action[0] === '/' && button.dataset.trusted !== 'true' && !confirm('Send "' + button.dataset.action + '"?')) { + if ( + button.dataset.action[0] === '/' && + button.dataset.trusted !== 'true' && + !confirm('Send "' + button.dataset.action + '"?') + ) { return; } client.sendMessage(button.dataset.action); @@ -124,7 +153,10 @@ const messageInteract = () => {