messaaging-room.form.vue rewrite to compositon api
This commit is contained in:
parent
9923cfaf50
commit
19af8e845f
4 changed files with 197 additions and 201 deletions
|
@ -17,7 +17,7 @@ import MkWindow from '@/components/ui/window.vue';
|
||||||
import MkEmojiPicker from '@/components/emoji-picker.vue';
|
import MkEmojiPicker from '@/components/emoji-picker.vue';
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
src?: HTMLElement;
|
src?: HTMLElement | EventTarget;
|
||||||
showPinned?: boolean;
|
showPinned?: boolean;
|
||||||
asReactionPicker?: boolean;
|
asReactionPicker?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
|
|
|
@ -422,7 +422,7 @@ type AwaitType<T> =
|
||||||
T;
|
T;
|
||||||
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
|
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
|
||||||
let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
|
let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
|
||||||
export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
|
export async function openEmojiPicker(src?: HTMLElement | EventTarget | null, opts, initialTextarea: typeof activeTextarea) {
|
||||||
if (openingEmojiPicker) return;
|
if (openingEmojiPicker) return;
|
||||||
|
|
||||||
activeTextarea = initialTextarea;
|
activeTextarea = initialTextarea;
|
||||||
|
|
|
@ -4,131 +4,121 @@
|
||||||
@drop.stop="onDrop"
|
@drop.stop="onDrop"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
ref="text"
|
ref="textEl"
|
||||||
v-model="text"
|
v-model="text"
|
||||||
:placeholder="$ts.inputMessageHere"
|
:placeholder="i18n.locale.inputMessageHere"
|
||||||
@keydown="onKeydown"
|
@keydown="onKeydown"
|
||||||
@compositionupdate="onCompositionUpdate"
|
@compositionupdate="onCompositionUpdate"
|
||||||
@paste="onPaste"
|
@paste="onPaste"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div v-if="file" class="file" @click="file = null">{{ file.name }}</div>
|
<div v-if="file" class="file" @click="file = null">{{ file.name }}</div>
|
||||||
<button class="send _button" :disabled="!canSend || sending" :title="$ts.send" @click="send">
|
<button class="send _button" :disabled="!canSend || sending" :title="i18n.locale.send" @click="send">
|
||||||
<template v-if="!sending"><i class="fas fa-paper-plane"></i></template><template v-if="sending"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>
|
<template v-if="!sending"><i class="fas fa-paper-plane"></i></template><template v-if="sending"><i class="fas fa-spinner fa-pulse fa-fw"></i></template>
|
||||||
</button>
|
</button>
|
||||||
<button class="_button" @click="chooseFile"><i class="fas fa-photo-video"></i></button>
|
<button class="_button" @click="chooseFile"><i class="fas fa-photo-video"></i></button>
|
||||||
<button class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
|
<button class="_button" @click="insertEmoji"><i class="fas fa-laugh-squint"></i></button>
|
||||||
<input ref="file" type="file" @change="onChangeFile"/>
|
<input ref="fileEl" type="file" @change="onChangeFile"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { defineComponent, defineAsyncComponent } from 'vue';
|
import { onMounted, watch } from 'vue';
|
||||||
import insertTextAtCursor from 'insert-text-at-cursor';
|
import * as Misskey from 'misskey-js';
|
||||||
import autosize from 'autosize';
|
import autosize from 'autosize';
|
||||||
|
import insertTextAtCursor from 'insert-text-at-cursor';
|
||||||
import { formatTimeString } from '@/scripts/format-time-string';
|
import { formatTimeString } from '@/scripts/format-time-string';
|
||||||
import { selectFile } from '@/scripts/select-file';
|
import { selectFile } from '@/scripts/select-file';
|
||||||
import * as os from '@/os';
|
import * as os from '@/os';
|
||||||
import { stream } from '@/stream';
|
import { stream } from '@/stream';
|
||||||
import { Autocomplete } from '@/scripts/autocomplete';
|
|
||||||
import { throttle } from 'throttle-debounce';
|
import { throttle } from 'throttle-debounce';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { Autocomplete } from '@/scripts/autocomplete';
|
||||||
|
|
||||||
export default defineComponent({
|
const props = defineProps<{
|
||||||
props: {
|
user?: Misskey.entities.UserDetailed | null;
|
||||||
user: {
|
group?: Misskey.entities.UserGroup | null;
|
||||||
type: Object,
|
}>();
|
||||||
requird: false,
|
|
||||||
},
|
let textEl = $ref<HTMLTextAreaElement>();
|
||||||
group: {
|
let fileEl = $ref<HTMLInputElement>();
|
||||||
type: Object,
|
|
||||||
requird: false,
|
let text: string = $ref('');
|
||||||
},
|
let file: Misskey.entities.DriveFile | null = $ref(null);
|
||||||
},
|
let sending = $ref(false);
|
||||||
data() {
|
const typing = throttle(3000, () => {
|
||||||
return {
|
stream.send('typingOnMessaging', props.user ? { partner: props.user.id } : { group: props.group?.id });
|
||||||
text: null,
|
});
|
||||||
file: null,
|
|
||||||
sending: false,
|
let draftKey = $computed(() => props.user ? 'user:' + props.user.id : 'group:' + props.group?.id);
|
||||||
typing: throttle(3000, () => {
|
let canSend = $computed(() => (text != null && text != '') || file != null);
|
||||||
stream.send('typingOnMessaging', this.user ? { partner: this.user.id } : { group: this.group.id });
|
|
||||||
}),
|
watch([$$(text), $$(file)], saveDraft);
|
||||||
};
|
|
||||||
},
|
onMounted(() => {
|
||||||
computed: {
|
autosize(textEl);
|
||||||
draftKey(): string {
|
|
||||||
return this.user ? 'user:' + this.user.id : 'group:' + this.group.id;
|
|
||||||
},
|
|
||||||
canSend(): boolean {
|
|
||||||
return (this.text != null && this.text != '') || this.file != null;
|
|
||||||
},
|
|
||||||
room(): any {
|
|
||||||
return this.$parent;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
text() {
|
|
||||||
this.saveDraft();
|
|
||||||
},
|
|
||||||
file() {
|
|
||||||
this.saveDraft();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
autosize(this.$refs.text);
|
|
||||||
|
|
||||||
// TODO: detach when unmount
|
// TODO: detach when unmount
|
||||||
// TODO
|
// TODO
|
||||||
//new Autocomplete(this.$refs.text, this, { model: 'text' });
|
//new Autocomplete(textEl, this, { model: 'text' });
|
||||||
|
|
||||||
// 書きかけの投稿を復元
|
// 書きかけの投稿を復元
|
||||||
const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[this.draftKey];
|
const draft = JSON.parse(localStorage.getItem('message_drafts') || '{}')[draftKey];
|
||||||
if (draft) {
|
if (draft) {
|
||||||
this.text = draft.data.text;
|
text = draft.data.text;
|
||||||
this.file = draft.data.file;
|
file = draft.data.file;
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
methods: {
|
|
||||||
async onPaste(e: ClipboardEvent) {
|
async function onPaste(e: ClipboardEvent) {
|
||||||
|
if (!e.clipboardData) return;
|
||||||
|
|
||||||
const data = e.clipboardData;
|
const data = e.clipboardData;
|
||||||
const items = data.items;
|
const items = data.items;
|
||||||
|
|
||||||
if (items.length == 1) {
|
if (items.length == 1) {
|
||||||
if (items[0].kind == 'file') {
|
if (items[0].kind == 'file') {
|
||||||
const file = items[0].getAsFile();
|
const file = items[0].getAsFile();
|
||||||
|
if (!file) return;
|
||||||
const lio = file.name.lastIndexOf('.');
|
const lio = file.name.lastIndexOf('.');
|
||||||
const ext = lio >= 0 ? file.name.slice(lio) : '';
|
const ext = lio >= 0 ? file.name.slice(lio) : '';
|
||||||
const formatted = `${formatTimeString(new Date(file.lastModified), this.$store.state.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
|
const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, '1')}${ext}`;
|
||||||
if (formatted) this.upload(file, formatted);
|
if (formatted) upload(file, formatted);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (items[0].kind == 'file') {
|
if (items[0].kind == 'file') {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: this.$ts.onlyOneFileCanBeAttached
|
text: i18n.locale.onlyOneFileCanBeAttached
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
function onDragover(e: DragEvent) {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
onDragover(e) {
|
|
||||||
const isFile = e.dataTransfer.items[0].kind == 'file';
|
const isFile = e.dataTransfer.items[0].kind == 'file';
|
||||||
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
||||||
if (isFile || isDriveFile) {
|
if (isFile || isDriveFile) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
|
function onDrop(e: DragEvent): void {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
onDrop(e): void {
|
|
||||||
// ファイルだったら
|
// ファイルだったら
|
||||||
if (e.dataTransfer.files.length == 1) {
|
if (e.dataTransfer.files.length == 1) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.upload(e.dataTransfer.files[0]);
|
upload(e.dataTransfer.files[0]);
|
||||||
return;
|
return;
|
||||||
} else if (e.dataTransfer.files.length > 1) {
|
} else if (e.dataTransfer.files.length > 1) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: this.$ts.onlyOneFileCanBeAttached
|
text: i18n.locale.onlyOneFileCanBeAttached
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -136,87 +126,90 @@ export default defineComponent({
|
||||||
//#region ドライブのファイル
|
//#region ドライブのファイル
|
||||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
if (driveFile != null && driveFile != '') {
|
if (driveFile != null && driveFile != '') {
|
||||||
this.file = JSON.parse(driveFile);
|
file = JSON.parse(driveFile);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
//#endregion
|
//#endregion
|
||||||
},
|
|
||||||
|
|
||||||
onKeydown(e) {
|
|
||||||
this.typing();
|
|
||||||
if ((e.which == 10 || e.which == 13) && (e.ctrlKey || e.metaKey) && this.canSend) {
|
|
||||||
this.send();
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
onCompositionUpdate() {
|
function onKeydown(e: KeyboardEvent) {
|
||||||
this.typing();
|
typing();
|
||||||
},
|
if ((e.key === 'Enter') && (e.ctrlKey || e.metaKey) && canSend) {
|
||||||
|
send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chooseFile(e) {
|
function onCompositionUpdate() {
|
||||||
selectFile(e.currentTarget || e.target, this.$ts.selectFile).then(file => {
|
typing();
|
||||||
this.file = file;
|
}
|
||||||
|
|
||||||
|
function chooseFile(e: MouseEvent) {
|
||||||
|
selectFile(e.currentTarget || e.target, i18n.locale.selectFile).then(file => {
|
||||||
|
file = file;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
onChangeFile() {
|
function onChangeFile() {
|
||||||
this.upload((this.$refs.file as any).files[0]);
|
if (fileEl?.files![0]) upload(fileEl.files[0]);
|
||||||
},
|
}
|
||||||
|
|
||||||
upload(file: File, name?: string) {
|
function upload(fileToUpload: File, name?: string) {
|
||||||
os.upload(file, this.$store.state.uploadFolder, name).then(res => {
|
os.upload(fileToUpload, defaultStore.state.uploadFolder, name).then(res => {
|
||||||
this.file = res;
|
file = res;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
send() {
|
function send() {
|
||||||
this.sending = true;
|
sending = true;
|
||||||
os.api('messaging/messages/create', {
|
os.api('messaging/messages/create', {
|
||||||
userId: this.user ? this.user.id : undefined,
|
userId: props.user ? props.user.id : undefined,
|
||||||
groupId: this.group ? this.group.id : undefined,
|
groupId: props.group ? props.group.id : undefined,
|
||||||
text: this.text ? this.text : undefined,
|
text: text ? text : undefined,
|
||||||
fileId: this.file ? this.file.id : undefined
|
fileId: file ? file.id : undefined
|
||||||
}).then(message => {
|
}).then(message => {
|
||||||
this.clear();
|
clear();
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.sending = false;
|
sending = false;
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
clear() {
|
function clear() {
|
||||||
this.text = '';
|
text = '';
|
||||||
this.file = null;
|
file = null;
|
||||||
this.deleteDraft();
|
deleteDraft();
|
||||||
},
|
}
|
||||||
|
|
||||||
saveDraft() {
|
function saveDraft() {
|
||||||
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
|
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
|
||||||
|
|
||||||
data[this.draftKey] = {
|
data[draftKey] = {
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
data: {
|
data: {
|
||||||
text: this.text,
|
text: text,
|
||||||
file: this.file
|
file: file
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('message_drafts', JSON.stringify(data));
|
localStorage.setItem('message_drafts', JSON.stringify(data));
|
||||||
},
|
}
|
||||||
|
|
||||||
deleteDraft() {
|
function deleteDraft() {
|
||||||
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
|
const data = JSON.parse(localStorage.getItem('message_drafts') || '{}');
|
||||||
|
|
||||||
delete data[this.draftKey];
|
delete data[draftKey];
|
||||||
|
|
||||||
localStorage.setItem('message_drafts', JSON.stringify(data));
|
localStorage.setItem('message_drafts', JSON.stringify(data));
|
||||||
},
|
}
|
||||||
|
|
||||||
async insertEmoji(ev) {
|
async function insertEmoji(ev: MouseEvent) {
|
||||||
os.openEmojiPicker(ev.currentTarget || ev.target, {}, this.$refs.text);
|
os.openEmojiPicker(ev.currentTarget || ev.target, {}, textEl);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
file,
|
||||||
|
upload,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ const props = defineProps<{
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let rootEl = $ref<Element>();
|
let rootEl = $ref<HTMLDivElement>();
|
||||||
let formEl = $ref<InstanceType<typeof XForm>>();
|
let formEl = $ref<InstanceType<typeof XForm>>();
|
||||||
let pagingComponent = $ref<InstanceType<typeof MkPagination>>();
|
let pagingComponent = $ref<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
|
@ -88,16 +88,18 @@ async function fetch() {
|
||||||
fetching = true;
|
fetching = true;
|
||||||
|
|
||||||
if (props.userAcct) {
|
if (props.userAcct) {
|
||||||
user = await os.api('users/show', Acct.parse(props.userAcct));
|
const acct = Acct.parse(props.userAcct);
|
||||||
|
user = await os.api('users/show', { username: acct.username, host: acct.host || undefined });
|
||||||
group = null;
|
group = null;
|
||||||
|
|
||||||
pagination = {
|
pagination = {
|
||||||
endpoint: 'messaging/messages',
|
endpoint: 'messaging/messages',
|
||||||
|
limit: 20,
|
||||||
params: {
|
params: {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
reversed: true,
|
reversed: true,
|
||||||
pageEl: $$(rootEl),
|
pageEl: $$(rootEl).value,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('messaging', {
|
connection = stream.useChannel('messaging', {
|
||||||
otherparty: user.id,
|
otherparty: user.id,
|
||||||
|
@ -108,14 +110,15 @@ async function fetch() {
|
||||||
|
|
||||||
pagination = {
|
pagination = {
|
||||||
endpoint: 'messaging/messages',
|
endpoint: 'messaging/messages',
|
||||||
|
limit: 20,
|
||||||
params: {
|
params: {
|
||||||
groupId: group.id,
|
groupId: group?.id,
|
||||||
},
|
},
|
||||||
reversed: true,
|
reversed: true,
|
||||||
pageEl: $$(rootEl),
|
pageEl: $$(rootEl).value,
|
||||||
};
|
};
|
||||||
connection = stream.useChannel('messaging', {
|
connection = stream.useChannel('messaging', {
|
||||||
group: group.id,
|
group: group?.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +127,7 @@ async function fetch() {
|
||||||
connection.on('read', onRead);
|
connection.on('read', onRead);
|
||||||
connection.on('deleted', onDeleted);
|
connection.on('deleted', onDeleted);
|
||||||
connection.on('typers', typers => {
|
connection.on('typers', typers => {
|
||||||
typers = typers.filter(u => u.id !== $i.id);
|
typers = typers.filter(u => u.id !== $i?.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', onVisibilitychange);
|
document.addEventListener('visibilitychange', onVisibilitychange);
|
||||||
|
@ -182,7 +185,7 @@ function onMessage(message) {
|
||||||
const _isBottom = isBottom(rootEl, 64);
|
const _isBottom = isBottom(rootEl, 64);
|
||||||
|
|
||||||
pagingComponent.prepend(message);
|
pagingComponent.prepend(message);
|
||||||
if (message.userId != $i.id && !document.hidden) {
|
if (message.userId != $i?.id && !document.hidden) {
|
||||||
connection?.send('read', {
|
connection?.send('read', {
|
||||||
id: message.id
|
id: message.id
|
||||||
});
|
});
|
||||||
|
@ -193,7 +196,7 @@ function onMessage(message) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
} else if (message.userId != $i.id) {
|
} else if (message.userId != $i?.id) {
|
||||||
// Notify
|
// Notify
|
||||||
notifyNewMessage();
|
notifyNewMessage();
|
||||||
}
|
}
|
||||||
|
@ -251,7 +254,7 @@ function notifyNewMessage() {
|
||||||
function onVisibilitychange() {
|
function onVisibilitychange() {
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
for (const message of pagingComponent.items) {
|
for (const message of pagingComponent.items) {
|
||||||
if (message.userId !== $i.id && !message.isRead) {
|
if (message.userId !== $i?.id && !message.isRead) {
|
||||||
connection?.send('read', {
|
connection?.send('read', {
|
||||||
id: message.id
|
id: message.id
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue