misskey/packages/frontend/src/pages/messaging/messaging-room.form.vue
tamaina d2204fd5c8
refactor: pagination/date-separated-list系処理を良い感じに? (#8209)
* pages/messaging/messaging-room.vue

* wip

* wip

* wip???

* wip?

* ✌️

* messaaging-room.form.vue rewrite to compositon api

* refactor

* 関心事でないのでとりあえず置いておく

* 🎨

* 🎨

* i18n.ts

* fix scroll container find function

* fix

* FIX

* ✌️

* Fix scroll bottom detect

* wip

* aaaaaaaaaaa

* rename

* fix

* fix?

* ✌️

* ✌️

* clean up

* clena up

* refactor

* scroll event once or not

* fix

* fix once

* add safe-area-inset-bottom to spacer

* fix

* ✌️

* 🎨

* fix

* fix

* wip

* ✌️

* clean up

* fix lint

* Update packages/client/src/components/global/sticky-container.vue

Co-authored-by: Johann150 <johann.galle@protonmail.com>

* Update packages/client/src/components/ui/pagination.vue

Co-authored-by: Johann150 <johann.galle@protonmail.com>

* Update packages/client/src/pages/messaging/messaging-room.form.vue

Co-authored-by: Johann150 <johann.galle@protonmail.com>

* clean up: single line comment

* https://github.com/misskey-dev/misskey/pull/8209#discussion_r867386077

* fix

* asobi → tolerance

* pick form

* pick message

* pick room

* fix lint

* fix scroll?

* fix scroll.ts

* fix directives/sticky-container

* update global/sticky-container.vue

* fix, 🎨

* revert merge

* ✌️

* fix lint errors

* 🎨

* Update packages/client/src/types/date-separated-list.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* https://github.com/misskey-dev/misskey/pull/8209#discussion_r917225080

* use '

* Update packages/client/src/scripts/scroll.ts

Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>

* use Number.EPSILON

Co-authored-by: acid-chicken <root@acid-chicken.com>

* revert

* fix

* fix

* Use % instead of vh

* 🎨

* 🎨

* 🎨

* wip

* wip

* css modules

Co-authored-by: Johann150 <johann.galle@protonmail.com>
Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
2023-01-13 18:25:40 +09:00

367 lines
8 KiB
Vue

<template>
<div
class="pemppnzi"
@dragover.stop="onDragover"
@drop.stop="onDrop"
>
<textarea
ref="textEl"
v-model="text"
:placeholder="i18n.ts.inputMessageHere"
@keydown="onKeydown"
@compositionupdate="onCompositionUpdate"
@paste="onPaste"
></textarea>
<footer>
<div v-if="file" class="file" @click="file = null">{{ file.name }}</div>
<div class="buttons">
<button class="_button" @click="chooseFile"><i class="ti ti-photo-plus"></i></button>
<button class="_button" @click="insertEmoji"><i class="ti ti-mood-happy"></i></button>
<button class="send _button" :disabled="!canSend || sending" :title="i18n.ts.send" @click="send">
<template v-if="!sending"><i class="ti ti-send"></i></template><template v-if="sending"><MkLoading :em="true"/></template>
</button>
</div>
</footer>
<input ref="fileEl" type="file" @change="onChangeFile"/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, watch } from 'vue';
import * as Misskey from 'misskey-js';
import autosize from 'autosize';
//import insertTextAtCursor from 'insert-text-at-cursor';
import { throttle } from 'throttle-debounce';
import { formatTimeString } from '@/scripts/format-time-string';
import { selectFile } from '@/scripts/select-file';
import * as os from '@/os';
import { stream } from '@/stream';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
//import { Autocomplete } from '@/scripts/autocomplete';
import { uploadFile } from '@/scripts/upload';
import { miLocalStorage } from '@/local-storage';
const props = defineProps<{
user?: Misskey.entities.UserDetailed | null;
group?: Misskey.entities.UserGroup | null;
}>();
let textEl = $shallowRef<HTMLTextAreaElement>();
let fileEl = $shallowRef<HTMLInputElement>();
let text = $ref<string>('');
let file = $ref<Misskey.entities.DriveFile | null>(null);
let sending = $ref(false);
const typing = throttle(3000, () => {
stream.send('typingOnMessaging', props.user ? { partner: props.user.id } : { group: props.group?.id });
});
let draftKey = $computed(() => props.user ? 'user:' + props.user.id : 'group:' + props.group?.id);
let canSend = $computed(() => (text != null && text !== '') || file != null);
watch([$$(text), $$(file)], saveDraft);
async function onPaste(ev: ClipboardEvent) {
if (!ev.clipboardData) return;
const clipboardData = ev.clipboardData;
const items = clipboardData.items;
if (items.length === 1) {
if (items[0].kind === 'file') {
const pastedFile = items[0].getAsFile();
if (!pastedFile) return;
const lio = pastedFile.name.lastIndexOf('.');
const ext = lio >= 0 ? pastedFile.name.slice(lio) : '';
const formatted = formatTimeString(new Date(pastedFile.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, '1') + ext;
if (formatted) upload(pastedFile, formatted);
}
} else {
if (items[0].kind === 'file') {
os.alert({
type: 'error',
text: i18n.ts.onlyOneFileCanBeAttached,
});
}
}
}
function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
const isFile = ev.dataTransfer.items[0].kind === 'file';
const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
if (isFile || isDriveFile) {
ev.preventDefault();
switch (ev.dataTransfer.effectAllowed) {
case 'all':
case 'uninitialized':
case 'copy':
case 'copyLink':
case 'copyMove':
ev.dataTransfer.dropEffect = 'copy';
break;
case 'linkMove':
case 'move':
ev.dataTransfer.dropEffect = 'move';
break;
default:
ev.dataTransfer.dropEffect = 'none';
break;
}
}
}
function onDrop(ev: DragEvent): void {
if (!ev.dataTransfer) return;
// ファイルだったら
if (ev.dataTransfer.files.length === 1) {
ev.preventDefault();
upload(ev.dataTransfer.files[0]);
return;
} else if (ev.dataTransfer.files.length > 1) {
ev.preventDefault();
os.alert({
type: 'error',
text: i18n.ts.onlyOneFileCanBeAttached,
});
return;
}
//#region ドライブのファイル
const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
if (driveFile != null && driveFile !== '') {
file = JSON.parse(driveFile);
ev.preventDefault();
}
//#endregion
}
function onKeydown(ev: KeyboardEvent) {
typing();
if ((ev.key === 'Enter') && (ev.ctrlKey || ev.metaKey) && canSend) {
send();
}
}
function onCompositionUpdate() {
typing();
}
function chooseFile(ev: MouseEvent) {
selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(selectedFile => {
file = selectedFile;
});
}
function onChangeFile() {
if (fileEl.files![0]) upload(fileEl.files[0]);
}
function upload(fileToUpload: File, name?: string) {
uploadFile(fileToUpload, defaultStore.state.uploadFolder, name).then(res => {
file = res;
});
}
function send() {
sending = true;
os.api('messaging/messages/create', {
userId: props.user ? props.user.id : undefined,
groupId: props.group ? props.group.id : undefined,
text: text ? text : undefined,
fileId: file ? file.id : undefined,
}).then(message => {
clear();
}).catch(err => {
console.error(err);
}).then(() => {
sending = false;
});
}
function clear() {
text = '';
file = null;
deleteDraft();
}
function saveDraft() {
const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}');
drafts[draftKey] = {
updatedAt: new Date(),
// eslint-disable-next-line id-denylist
data: {
text: text,
file: file,
},
};
miLocalStorage.setItem('message_drafts', JSON.stringify(drafts));
}
function deleteDraft() {
const drafts = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}');
delete drafts[draftKey];
miLocalStorage.setItem('message_drafts', JSON.stringify(drafts));
}
async function insertEmoji(ev: MouseEvent) {
os.openEmojiPicker(ev.currentTarget ?? ev.target, {}, textEl);
}
onMounted(() => {
autosize(textEl);
// TODO: detach when unmount
// TODO
//new Autocomplete(textEl, this, { model: 'text' });
// 書きかけの投稿を復元
const draft = JSON.parse(miLocalStorage.getItem('message_drafts') || '{}')[draftKey];
if (draft) {
text = draft.data.text;
file = draft.data.file;
}
});
defineExpose({
file,
upload,
});
</script>
<style lang="scss" scoped>
.pemppnzi {
position: relative;
> textarea {
cursor: auto;
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
min-height: 80px;
margin: 0;
padding: 16px 16px 0 16px;
resize: none;
font-size: 1em;
font-family: inherit;
outline: none;
border: none;
border-radius: 0;
box-shadow: none;
box-sizing: border-box;
color: var(--fg);
background: rgba(12, 18, 16, 0.85);
backdrop-filter: var(--blur, blur(15px));
}
footer {
position: sticky;
bottom: 0;
background: var(--panel);
> .file {
padding: 8px;
color: var(--fg);
background: transparent;
cursor: pointer;
}
}
.files {
display: block;
margin: 0;
padding: 0 8px;
list-style: none;
&:after {
content: '';
display: block;
clear: both;
}
> li {
display: block;
float: left;
margin: 4px;
padding: 0;
width: 64px;
height: 64px;
background-color: #eee;
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
cursor: move;
&:hover {
> .remove {
display: block;
}
}
> .remove {
display: none;
position: absolute;
right: -6px;
top: -6px;
margin: 0;
padding: 0;
background: transparent;
outline: none;
border: none;
border-radius: 0;
box-shadow: none;
cursor: pointer;
}
}
}
.buttons {
display: flex;
._button {
margin: 0;
padding: 16px;
font-size: 1em;
font-weight: normal;
text-decoration: none;
transition: color 0.1s ease;
&:hover {
color: var(--accent);
}
&:active {
color: var(--accentDarken);
transition: color 0s ease;
}
}
> .send {
margin-left: auto;
color: var(--accent);
&:hover {
color: var(--accentLighten);
}
&:active {
color: var(--accentDarken);
transition: color 0s ease;
}
}
}
input[type=file] {
display: none;
}
}
</style>