pages/messaging/messaging-room.vue
This commit is contained in:
parent
47edc18931
commit
a1f346a549
2 changed files with 220 additions and 276 deletions
|
@ -62,10 +62,6 @@ function dragClear(fn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
provide: {
|
|
||||||
inWindow: true
|
|
||||||
},
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
padding: {
|
padding: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="_section"
|
<div class="_section"
|
||||||
@dragover.prevent.stop="onDragover"
|
@dragover.prevent.stop="onDragover"
|
||||||
@drop.prevent.stop="onDrop"
|
@drop.prevent.stop="onDrop"
|
||||||
|
ref="rootEl"
|
||||||
>
|
>
|
||||||
<div class="_content mk-messaging-room">
|
<div class="_content mk-messaging-room">
|
||||||
<div class="body">
|
<div class="body">
|
||||||
|
@ -35,9 +36,10 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts" setup>
|
||||||
import { computed, defineComponent, markRaw } from 'vue';
|
import { computed, watch, onMounted, nextTick, onBeforeUnmount } from 'vue';
|
||||||
import XList from '@/components/date-separated-list.vue';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import MkPagination from '@/components/ui/pagination.vue';
|
||||||
import XMessage from './messaging-room.message.vue';
|
import XMessage from './messaging-room.message.vue';
|
||||||
import XForm from './messaging-room.form.vue';
|
import XForm from './messaging-room.form.vue';
|
||||||
import * as Acct from 'misskey-js/built/acct';
|
import * as Acct from 'misskey-js/built/acct';
|
||||||
|
@ -47,300 +49,246 @@ import { stream } from '@/stream';
|
||||||
import { popout } from '@/scripts/popout';
|
import { popout } from '@/scripts/popout';
|
||||||
import * as sound from '@/scripts/sound';
|
import * as sound from '@/scripts/sound';
|
||||||
import * as symbols from '@/symbols';
|
import * as symbols from '@/symbols';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { router } from '@/router';
|
||||||
|
|
||||||
const Component = defineComponent({
|
const props = defineProps<{
|
||||||
components: {
|
userAcct?: string;
|
||||||
XMessage,
|
groupId?: string;
|
||||||
XForm,
|
}>();
|
||||||
XList,
|
|
||||||
},
|
|
||||||
|
|
||||||
inject: ['inWindow'],
|
let fetching = $ref(true);
|
||||||
|
let user: Misskey.entities.UserDetailed | null = $ref(null);
|
||||||
|
let group: Misskey.entities.UserGroup | null = $ref(null);
|
||||||
|
let fetchingMoreMessages = $ref(false);
|
||||||
|
let messages = $ref<Misskey.entities.MessagingMessage[]>([]);
|
||||||
|
let existMoreMessages = $ref(false);
|
||||||
|
let connection: Misskey.ChannelConnection<Misskey.Channels['messaging']> | null = $ref(null);
|
||||||
|
let showIndicator = $ref(false);
|
||||||
|
let timer: number | null = $ref(null);
|
||||||
|
const ilObserver = new IntersectionObserver(
|
||||||
|
(entries) => entries.some((entry) => entry.isIntersecting)
|
||||||
|
&& !fetching
|
||||||
|
&& !fetchingMoreMessages
|
||||||
|
&& existMoreMessages
|
||||||
|
&& fetchMoreMessages()
|
||||||
|
);
|
||||||
|
|
||||||
props: {
|
let rootEl = $ref<Element>();
|
||||||
userAcct: {
|
let form = $ref<InstanceType<typeof XForm>>();
|
||||||
type: String,
|
let loadMore = $ref<HTMLDivElement>();
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
groupId: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
watch([() => props.userAcct, () => props.groupId], () => {
|
||||||
return {
|
if (connection) connection.dispose();
|
||||||
[symbols.PAGE_INFO]: computed(() => !this.fetching ? this.user ? {
|
fetch();
|
||||||
userName: this.user,
|
});
|
||||||
avatar: this.user,
|
|
||||||
action: {
|
|
||||||
icon: 'fas fa-ellipsis-h',
|
|
||||||
handler: this.menu,
|
|
||||||
},
|
|
||||||
} : {
|
|
||||||
title: this.group.name,
|
|
||||||
icon: 'fas fa-users',
|
|
||||||
action: {
|
|
||||||
icon: 'fas fa-ellipsis-h',
|
|
||||||
handler: this.menu,
|
|
||||||
},
|
|
||||||
} : null),
|
|
||||||
fetching: true,
|
|
||||||
user: null,
|
|
||||||
group: null,
|
|
||||||
fetchingMoreMessages: false,
|
|
||||||
messages: [],
|
|
||||||
existMoreMessages: false,
|
|
||||||
connection: null,
|
|
||||||
showIndicator: false,
|
|
||||||
timer: null,
|
|
||||||
typers: [],
|
|
||||||
ilObserver: new IntersectionObserver(
|
|
||||||
(entries) => entries.some((entry) => entry.isIntersecting)
|
|
||||||
&& !this.fetching
|
|
||||||
&& !this.fetchingMoreMessages
|
|
||||||
&& this.existMoreMessages
|
|
||||||
&& this.fetchMoreMessages()
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
async function fetch() {
|
||||||
form(): any {
|
fetching = true;
|
||||||
return this.$refs.form;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
connection = stream.useChannel('messaging', {
|
||||||
userAcct: 'fetch',
|
otherparty: user ? user.id : undefined,
|
||||||
groupId: 'fetch',
|
group: group ? group.id : undefined,
|
||||||
},
|
});
|
||||||
|
|
||||||
mounted() {
|
connection?.on('message', onMessage);
|
||||||
this.fetch();
|
connection?.on('read', onRead);
|
||||||
if (this.$store.state.enableInfiniteScroll) {
|
connection?.on('deleted', onDeleted);
|
||||||
this.$nextTick(() => this.ilObserver.observe(this.$refs.loadMore as Element));
|
connection?.on('typers', typers => {
|
||||||
}
|
typers = typers.filter(u => u.id !== $i.id);
|
||||||
},
|
});
|
||||||
|
|
||||||
beforeUnmount() {
|
document.addEventListener('visibilitychange', onVisibilitychange);
|
||||||
this.connection.dispose();
|
|
||||||
|
|
||||||
document.removeEventListener('visibilitychange', this.onVisibilitychange);
|
fetchMessages().then(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
this.ilObserver.disconnect();
|
// もっと見るの交差検知を発火させないためにfetchは
|
||||||
},
|
// スクロールが終わるまでfalseにしておく
|
||||||
|
// scrollendのようなイベントはないのでsetTimeoutで
|
||||||
|
window.setTimeout(() => fetching = false, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
methods: {
|
function onDragover(e: DragEvent) {
|
||||||
async fetch() {
|
if (!e.dataTransfer) return;
|
||||||
this.fetching = true;
|
|
||||||
if (this.userAcct) {
|
const isFile = e.dataTransfer.items[0].kind == 'file';
|
||||||
const user = await os.api('users/show', Acct.parse(this.userAcct));
|
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
||||||
this.user = user;
|
|
||||||
|
if (isFile || isDriveFile) {
|
||||||
|
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
||||||
|
} else {
|
||||||
|
e.dataTransfer.dropEffect = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(e: DragEvent): void {
|
||||||
|
if (!e.dataTransfer) return;
|
||||||
|
|
||||||
|
// ファイルだったら
|
||||||
|
if (e.dataTransfer.files.length == 1) {
|
||||||
|
form.upload(e.dataTransfer.files[0]);
|
||||||
|
return;
|
||||||
|
} else if (e.dataTransfer.files.length > 1) {
|
||||||
|
os.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: i18n.locale.onlyOneFileCanBeAttached
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#region ドライブのファイル
|
||||||
|
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
||||||
|
if (driveFile != null && driveFile != '') {
|
||||||
|
const file = JSON.parse(driveFile);
|
||||||
|
form.file = file;
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchMessages() {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const max = existMoreMessages ? 20 : 10;
|
||||||
|
|
||||||
|
os.api('messaging/messages', {
|
||||||
|
userId: user ? user.id : undefined,
|
||||||
|
groupId: group ? group.id : undefined,
|
||||||
|
limit: max + 1,
|
||||||
|
untilId: existMoreMessages ? messages[0].id : undefined
|
||||||
|
}).then(messages => {
|
||||||
|
if (messages.length == max + 1) {
|
||||||
|
existMoreMessages = true;
|
||||||
|
messages.pop();
|
||||||
} else {
|
} else {
|
||||||
const group = await os.api('users/groups/show', { groupId: this.groupId });
|
existMoreMessages = false;
|
||||||
this.group = group;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connection = markRaw(stream.useChannel('messaging', {
|
messages.unshift.apply(messages, messages.reverse());
|
||||||
otherparty: this.user ? this.user.id : undefined,
|
resolve();
|
||||||
group: this.group ? this.group.id : undefined,
|
});
|
||||||
}));
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.connection.on('message', this.onMessage);
|
function fetchMoreMessages() {
|
||||||
this.connection.on('read', this.onRead);
|
fetchingMoreMessages = true;
|
||||||
this.connection.on('deleted', this.onDeleted);
|
fetchMessages().then(() => {
|
||||||
this.connection.on('typers', typers => {
|
fetchingMoreMessages = false;
|
||||||
this.typers = typers.filter(u => u.id !== this.$i.id);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', this.onVisibilitychange);
|
function onMessage(message) {
|
||||||
|
sound.play('chat');
|
||||||
|
|
||||||
this.fetchMessages().then(() => {
|
const _isBottom = isBottom(rootEl, 64);
|
||||||
this.scrollToBottom();
|
|
||||||
|
|
||||||
// もっと見るの交差検知を発火させないためにfetchは
|
messages.push(message);
|
||||||
// スクロールが終わるまでfalseにしておく
|
if (message.userId != $i.id && !document.hidden) {
|
||||||
// scrollendのようなイベントはないのでsetTimeoutで
|
connection?.send('read', {
|
||||||
window.setTimeout(() => this.fetching = false, 300);
|
id: message.id
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
|
|
||||||
onDragover(e) {
|
if (_isBottom) {
|
||||||
const isFile = e.dataTransfer.items[0].kind == 'file';
|
// Scroll to bottom
|
||||||
const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
|
nextTick(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
});
|
||||||
|
} else if (message.userId != $i.id) {
|
||||||
|
// Notify
|
||||||
|
notifyNewMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isFile || isDriveFile) {
|
function onRead(x) {
|
||||||
e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
|
if (user) {
|
||||||
} else {
|
if (!Array.isArray(x)) x = [x];
|
||||||
e.dataTransfer.dropEffect = 'none';
|
for (const id of x) {
|
||||||
|
if (messages.some(x => x.id == id)) {
|
||||||
|
const exist = messages.map(x => x.id).indexOf(id);
|
||||||
|
messages[exist] = {
|
||||||
|
...messages[exist],
|
||||||
|
isRead: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
onDrop(e): void {
|
|
||||||
// ファイルだったら
|
|
||||||
if (e.dataTransfer.files.length == 1) {
|
|
||||||
this.form.upload(e.dataTransfer.files[0]);
|
|
||||||
return;
|
|
||||||
} else if (e.dataTransfer.files.length > 1) {
|
|
||||||
os.alert({
|
|
||||||
type: 'error',
|
|
||||||
text: this.$ts.onlyOneFileCanBeAttached
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//#region ドライブのファイル
|
|
||||||
const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
|
|
||||||
if (driveFile != null && driveFile != '') {
|
|
||||||
const file = JSON.parse(driveFile);
|
|
||||||
this.form.file = file;
|
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchMessages() {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const max = this.existMoreMessages ? 20 : 10;
|
|
||||||
|
|
||||||
os.api('messaging/messages', {
|
|
||||||
userId: this.user ? this.user.id : undefined,
|
|
||||||
groupId: this.group ? this.group.id : undefined,
|
|
||||||
limit: max + 1,
|
|
||||||
untilId: this.existMoreMessages ? this.messages[0].id : undefined
|
|
||||||
}).then(messages => {
|
|
||||||
if (messages.length == max + 1) {
|
|
||||||
this.existMoreMessages = true;
|
|
||||||
messages.pop();
|
|
||||||
} else {
|
|
||||||
this.existMoreMessages = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.messages.unshift.apply(this.messages, messages.reverse());
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
fetchMoreMessages() {
|
|
||||||
this.fetchingMoreMessages = true;
|
|
||||||
this.fetchMessages().then(() => {
|
|
||||||
this.fetchingMoreMessages = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onMessage(message) {
|
|
||||||
sound.play('chat');
|
|
||||||
|
|
||||||
const _isBottom = isBottom(this.$el, 64);
|
|
||||||
|
|
||||||
this.messages.push(message);
|
|
||||||
if (message.userId != this.$i.id && !document.hidden) {
|
|
||||||
this.connection.send('read', {
|
|
||||||
id: message.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_isBottom) {
|
|
||||||
// Scroll to bottom
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.scrollToBottom();
|
|
||||||
});
|
|
||||||
} else if (message.userId != this.$i.id) {
|
|
||||||
// Notify
|
|
||||||
this.notifyNewMessage();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onRead(x) {
|
|
||||||
if (this.user) {
|
|
||||||
if (!Array.isArray(x)) x = [x];
|
|
||||||
for (const id of x) {
|
|
||||||
if (this.messages.some(x => x.id == id)) {
|
|
||||||
const exist = this.messages.map(x => x.id).indexOf(id);
|
|
||||||
this.messages[exist] = {
|
|
||||||
...this.messages[exist],
|
|
||||||
isRead: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this.group) {
|
|
||||||
for (const id of x.ids) {
|
|
||||||
if (this.messages.some(x => x.id == id)) {
|
|
||||||
const exist = this.messages.map(x => x.id).indexOf(id);
|
|
||||||
this.messages[exist] = {
|
|
||||||
...this.messages[exist],
|
|
||||||
reads: [...this.messages[exist].reads, x.userId]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onDeleted(id) {
|
|
||||||
const msg = this.messages.find(m => m.id === id);
|
|
||||||
if (msg) {
|
|
||||||
this.messages = this.messages.filter(m => m.id !== msg.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
scrollToBottom() {
|
|
||||||
scroll(this.$el, { top: this.$el.offsetHeight });
|
|
||||||
},
|
|
||||||
|
|
||||||
onIndicatorClick() {
|
|
||||||
this.showIndicator = false;
|
|
||||||
this.scrollToBottom();
|
|
||||||
},
|
|
||||||
|
|
||||||
notifyNewMessage() {
|
|
||||||
this.showIndicator = true;
|
|
||||||
|
|
||||||
onScrollBottom(this.$el, () => {
|
|
||||||
this.showIndicator = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.timer) window.clearTimeout(this.timer);
|
|
||||||
|
|
||||||
this.timer = window.setTimeout(() => {
|
|
||||||
this.showIndicator = false;
|
|
||||||
}, 4000);
|
|
||||||
},
|
|
||||||
|
|
||||||
onVisibilitychange() {
|
|
||||||
if (document.hidden) return;
|
|
||||||
for (const message of this.messages) {
|
|
||||||
if (message.userId !== this.$i.id && !message.isRead) {
|
|
||||||
this.connection.send('read', {
|
|
||||||
id: message.id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
menu(ev) {
|
|
||||||
const path = this.groupId ? `/my/messaging/group/${this.groupId}` : `/my/messaging/${this.userAcct}`;
|
|
||||||
|
|
||||||
os.popupMenu([this.inWindow ? undefined : {
|
|
||||||
text: this.$ts.openInWindow,
|
|
||||||
icon: 'fas fa-window-maximize',
|
|
||||||
action: () => {
|
|
||||||
os.pageWindow(path);
|
|
||||||
this.$router.back();
|
|
||||||
},
|
|
||||||
}, this.inWindow ? undefined : {
|
|
||||||
text: this.$ts.popout,
|
|
||||||
icon: 'fas fa-external-link-alt',
|
|
||||||
action: () => {
|
|
||||||
popout(path);
|
|
||||||
this.$router.back();
|
|
||||||
},
|
|
||||||
}], ev.currentTarget || ev.target);
|
|
||||||
}
|
}
|
||||||
|
} else if (group) {
|
||||||
|
for (const id of x.ids) {
|
||||||
|
if (messages.some(x => x.id == id)) {
|
||||||
|
const exist = messages.map(x => x.id).indexOf(id);
|
||||||
|
messages[exist] = {
|
||||||
|
...messages[exist],
|
||||||
|
reads: [...messages[exist].reads, x.userId]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDeleted(id) {
|
||||||
|
const msg = messages.find(m => m.id === id);
|
||||||
|
if (msg) {
|
||||||
|
messages = messages.filter(m => m.id !== msg.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
scroll(rootEl, { top: rootEl.offsetHeight });
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIndicatorClick() {
|
||||||
|
showIndicator = false;
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyNewMessage() {
|
||||||
|
showIndicator = true;
|
||||||
|
|
||||||
|
onScrollBottom(rootEl, () => {
|
||||||
|
showIndicator = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timer) window.clearTimeout(timer);
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
showIndicator = false;
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVisibilitychange() {
|
||||||
|
if (document.hidden) return;
|
||||||
|
for (const message of messages) {
|
||||||
|
if (message.userId !== $i.id && !message.isRead) {
|
||||||
|
connection?.send('read', {
|
||||||
|
id: message.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetch();
|
||||||
|
if (defaultStore.state.enableInfiniteScroll) {
|
||||||
|
nextTick(() => ilObserver.observe(loadMore));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Component;
|
onBeforeUnmount(() => {
|
||||||
|
connection?.dispose();
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilitychange);
|
||||||
|
ilObserver.disconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
[symbols.PAGE_INFO]: computed(() => !fetching ? user ? {
|
||||||
|
userName: user,
|
||||||
|
avatar: user,
|
||||||
|
} : {
|
||||||
|
title: group?.name,
|
||||||
|
icon: 'fas fa-users',
|
||||||
|
} : null),
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue