parent
0d4feed6d3
commit
7c0806f208
|
@ -4,6 +4,7 @@
|
||||||
-
|
-
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Feat: デッキにチャットカラムを追加
|
||||||
- Fix: ログアウトした際に処理が終了しない問題を修正
|
- Fix: ログアウトした際に処理が終了しない問題を修正
|
||||||
- Fix: 自動バックアップが設定されている環境でログアウト直前に設定をバックアップするように
|
- Fix: 自動バックアップが設定されている環境でログアウト直前に設定をバックアップするように
|
||||||
|
|
||||||
|
|
|
@ -10230,6 +10230,10 @@ export interface Locale extends ILocale {
|
||||||
* ロールタイムライン
|
* ロールタイムライン
|
||||||
*/
|
*/
|
||||||
"roleTimeline": string;
|
"roleTimeline": string;
|
||||||
|
/**
|
||||||
|
* チャット
|
||||||
|
*/
|
||||||
|
"chat": string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"_dialog": {
|
"_dialog": {
|
||||||
|
|
|
@ -2705,6 +2705,7 @@ _deck:
|
||||||
mentions: "あなた宛て"
|
mentions: "あなた宛て"
|
||||||
direct: "ダイレクト"
|
direct: "ダイレクト"
|
||||||
roleTimeline: "ロールタイムライン"
|
roleTimeline: "ロールタイムライン"
|
||||||
|
chat: "チャット"
|
||||||
|
|
||||||
_dialog:
|
_dialog:
|
||||||
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
|
||||||
|
|
|
@ -0,0 +1,208 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="history.length > 0" class="_gaps_s">
|
||||||
|
<MkA
|
||||||
|
v-for="item in history"
|
||||||
|
:key="item.id"
|
||||||
|
:class="[$style.message, { [$style.isMe]: item.isMe, [$style.isRead]: item.message.isRead }]"
|
||||||
|
class="_panel"
|
||||||
|
:to="item.message.toRoomId ? `/chat/room/${item.message.toRoomId}` : `/chat/user/${item.other!.id}`"
|
||||||
|
>
|
||||||
|
<MkAvatar v-if="item.message.toRoomId" :class="$style.messageAvatar" :user="item.message.fromUser" indicator :preview="false"/>
|
||||||
|
<MkAvatar v-else-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/>
|
||||||
|
<div :class="$style.messageBody">
|
||||||
|
<header v-if="item.message.toRoom" :class="$style.messageHeader">
|
||||||
|
<span :class="$style.messageHeaderName"><i class="ti ti-users"></i> {{ item.message.toRoom.name }}</span>
|
||||||
|
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
||||||
|
</header>
|
||||||
|
<header v-else :class="$style.messageHeader">
|
||||||
|
<MkUserName :class="$style.messageHeaderName" :user="item.other!"/>
|
||||||
|
<MkAcct :class="$style.messageHeaderUsername" :user="item.other!"/>
|
||||||
|
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
||||||
|
</header>
|
||||||
|
<div :class="$style.messageBodyText"><span v-if="item.isMe" :class="$style.youSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</div>
|
||||||
|
</div>
|
||||||
|
</MkA>
|
||||||
|
</div>
|
||||||
|
<div v-if="!initializing && history.length == 0" class="_fullinfo">
|
||||||
|
<div>{{ i18n.ts._chat.noHistory }}</div>
|
||||||
|
</div>
|
||||||
|
<MkLoading v-if="initializing"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onDeactivated, onMounted, ref } from 'vue';
|
||||||
|
import * as Misskey from 'misskey-js';
|
||||||
|
import { useInterval } from '@@/js/use-interval.js';
|
||||||
|
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||||
|
import { i18n } from '@/i18n.js';
|
||||||
|
import { ensureSignin } from '@/i.js';
|
||||||
|
|
||||||
|
const $i = ensureSignin();
|
||||||
|
|
||||||
|
const history = ref<{
|
||||||
|
id: string;
|
||||||
|
message: Misskey.entities.ChatMessage;
|
||||||
|
other: Misskey.entities.ChatMessage['fromUser'] | Misskey.entities.ChatMessage['toUser'] | null;
|
||||||
|
isMe: boolean;
|
||||||
|
}[]>([]);
|
||||||
|
|
||||||
|
const initializing = ref(true);
|
||||||
|
const fetching = ref(false);
|
||||||
|
|
||||||
|
async function fetchHistory() {
|
||||||
|
if (fetching.value) return;
|
||||||
|
|
||||||
|
fetching.value = true;
|
||||||
|
|
||||||
|
const [userMessages, roomMessages] = await Promise.all([
|
||||||
|
misskeyApi('chat/history', { room: false }),
|
||||||
|
misskeyApi('chat/history', { room: true }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
history.value = [...userMessages, ...roomMessages]
|
||||||
|
.toSorted((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
|
.map(m => ({
|
||||||
|
id: m.id,
|
||||||
|
message: m,
|
||||||
|
other: (!('room' in m) || m.room == null) ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
|
||||||
|
isMe: m.fromUserId === $i.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
fetching.value = false;
|
||||||
|
initializing.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isActivated = true;
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
isActivated = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
isActivated = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
// TODO: DOM的にバックグラウンドになっていないかどうかも考慮する
|
||||||
|
if (!window.document.hidden && isActivated) {
|
||||||
|
fetchHistory();
|
||||||
|
}
|
||||||
|
}, 1000 * 10, {
|
||||||
|
immediate: false,
|
||||||
|
afterMounted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
fetchHistory();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchHistory();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.message {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
padding: 16px 24px;
|
||||||
|
|
||||||
|
&.isRead,
|
||||||
|
&.isMe {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.isMe):not(.isRead) {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
background-color: var(--MI_THEME-accent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 500px) {
|
||||||
|
.message {
|
||||||
|
font-size: 90%;
|
||||||
|
padding: 14px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 450px) {
|
||||||
|
.message {
|
||||||
|
font-size: 80%;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageAvatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin: 0 16px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 500px) {
|
||||||
|
.messageAvatar {
|
||||||
|
width: 45px;
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 450px) {
|
||||||
|
.messageAvatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageBody {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageHeaderName {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageHeaderUsername {
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageHeaderTime {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messageBodyText {
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youSaid {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -38,6 +38,7 @@ export const columnTypes = [
|
||||||
'mentions',
|
'mentions',
|
||||||
'direct',
|
'direct',
|
||||||
'roleTimeline',
|
'roleTimeline',
|
||||||
|
'chat',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ColumnType = typeof columnTypes[number];
|
export type ColumnType = typeof columnTypes[number];
|
||||||
|
|
|
@ -34,34 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkFoldableSection>
|
<MkFoldableSection>
|
||||||
<template #header>{{ i18n.ts._chat.history }}</template>
|
<template #header>{{ i18n.ts._chat.history }}</template>
|
||||||
|
|
||||||
<div v-if="history.length > 0" class="_gaps_s">
|
<MkChatHistories/>
|
||||||
<MkA
|
|
||||||
v-for="item in history"
|
|
||||||
:key="item.id"
|
|
||||||
:class="[$style.message, { [$style.isMe]: item.isMe, [$style.isRead]: item.message.isRead }]"
|
|
||||||
class="_panel"
|
|
||||||
:to="item.message.toRoomId ? `/chat/room/${item.message.toRoomId}` : `/chat/user/${item.other!.id}`"
|
|
||||||
>
|
|
||||||
<MkAvatar v-if="item.message.toRoomId" :class="$style.messageAvatar" :user="item.message.fromUser" indicator :preview="false"/>
|
|
||||||
<MkAvatar v-else-if="item.other" :class="$style.messageAvatar" :user="item.other" indicator :preview="false"/>
|
|
||||||
<div :class="$style.messageBody">
|
|
||||||
<header v-if="item.message.toRoom" :class="$style.messageHeader">
|
|
||||||
<span :class="$style.messageHeaderName"><i class="ti ti-users"></i> {{ item.message.toRoom.name }}</span>
|
|
||||||
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
|
||||||
</header>
|
|
||||||
<header v-else :class="$style.messageHeader">
|
|
||||||
<MkUserName :class="$style.messageHeaderName" :user="item.other!"/>
|
|
||||||
<MkAcct :class="$style.messageHeaderUsername" :user="item.other!"/>
|
|
||||||
<MkTime :time="item.message.createdAt" :class="$style.messageHeaderTime"/>
|
|
||||||
</header>
|
|
||||||
<div :class="$style.messageBodyText"><span v-if="item.isMe" :class="$style.youSaid">{{ i18n.ts.you }}:</span>{{ item.message.text }}</div>
|
|
||||||
</div>
|
|
||||||
</MkA>
|
|
||||||
</div>
|
|
||||||
<div v-if="!initializing && history.length == 0" class="_fullinfo">
|
|
||||||
<div>{{ i18n.ts._chat.noHistory }}</div>
|
|
||||||
</div>
|
|
||||||
<MkLoading v-if="initializing"/>
|
|
||||||
</MkFoldableSection>
|
</MkFoldableSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -81,20 +54,12 @@ import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
import MkChatHistories from '@/components/MkChatHistories.vue';
|
||||||
|
|
||||||
const $i = ensureSignin();
|
const $i = ensureSignin();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const initializing = ref(true);
|
|
||||||
const fetching = ref(false);
|
|
||||||
const history = ref<{
|
|
||||||
id: string;
|
|
||||||
message: Misskey.entities.ChatMessage;
|
|
||||||
other: Misskey.entities.ChatMessage['fromUser'] | Misskey.entities.ChatMessage['toUser'] | null;
|
|
||||||
isMe: boolean;
|
|
||||||
}[]>([]);
|
|
||||||
|
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const searched = ref(false);
|
const searched = ref(false);
|
||||||
const searchResults = ref<Misskey.entities.ChatMessage[]>([]);
|
const searchResults = ref<Misskey.entities.ChatMessage[]>([]);
|
||||||
|
@ -148,57 +113,8 @@ async function search() {
|
||||||
searched.value = true;
|
searched.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchHistory() {
|
|
||||||
if (fetching.value) return;
|
|
||||||
|
|
||||||
fetching.value = true;
|
|
||||||
|
|
||||||
const [userMessages, roomMessages] = await Promise.all([
|
|
||||||
misskeyApi('chat/history', { room: false }),
|
|
||||||
misskeyApi('chat/history', { room: true }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
history.value = [...userMessages, ...roomMessages]
|
|
||||||
.toSorted((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
|
||||||
.map(m => ({
|
|
||||||
id: m.id,
|
|
||||||
message: m,
|
|
||||||
other: (!('room' in m) || m.room == null) ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
|
|
||||||
isMe: m.fromUserId === $i.id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
fetching.value = false;
|
|
||||||
initializing.value = false;
|
|
||||||
|
|
||||||
updateCurrentAccountPartial({ hasUnreadChatMessages: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
let isActivated = true;
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
isActivated = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
onDeactivated(() => {
|
|
||||||
isActivated = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
useInterval(() => {
|
|
||||||
// TODO: DOM的にバックグラウンドになっていないかどうかも考慮する
|
|
||||||
if (!window.document.hidden && isActivated) {
|
|
||||||
fetchHistory();
|
|
||||||
}
|
|
||||||
}, 1000 * 10, {
|
|
||||||
immediate: false,
|
|
||||||
afterMounted: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
onActivated(() => {
|
|
||||||
fetchHistory();
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchHistory();
|
updateCurrentAccountPartial({ hasUnreadChatMessages: false });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -207,77 +123,6 @@ onMounted(() => {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
padding: 16px 24px;
|
|
||||||
|
|
||||||
&.isRead,
|
|
||||||
&.isMe {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:not(.isMe):not(.isRead) {
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: var(--MI_THEME-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageAvatar {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
margin: 0 16px 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageBody {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageHeader {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: clip;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageHeaderName {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageHeaderUsername {
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageHeaderTime {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.messageBodyText {
|
|
||||||
overflow: hidden;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
font-size: 1.1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.youSaid {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-right: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchResultItem {
|
.searchResultItem {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: solid 1px var(--MI_THEME-divider);
|
border: solid 1px var(--MI_THEME-divider);
|
||||||
|
|
|
@ -97,6 +97,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue';
|
||||||
import XMentionsColumn from '@/ui/deck/mentions-column.vue';
|
import XMentionsColumn from '@/ui/deck/mentions-column.vue';
|
||||||
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
import XDirectColumn from '@/ui/deck/direct-column.vue';
|
||||||
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
|
||||||
|
import XChatColumn from '@/ui/deck/chat-column.vue';
|
||||||
import { mainRouter } from '@/router.js';
|
import { mainRouter } from '@/router.js';
|
||||||
import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
|
import { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
|
||||||
|
|
||||||
|
@ -114,6 +115,7 @@ const columnComponents = {
|
||||||
mentions: XMentionsColumn,
|
mentions: XMentionsColumn,
|
||||||
direct: XDirectColumn,
|
direct: XDirectColumn,
|
||||||
roleTimeline: XRoleTimelineColumn,
|
roleTimeline: XRoleTimelineColumn,
|
||||||
|
chat: XChatColumn,
|
||||||
};
|
};
|
||||||
|
|
||||||
mainRouter.navHook = (path, flag): boolean => {
|
mainRouter.navHook = (path, flag): boolean => {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<XColumn :column="column" :isStacked="isStacked">
|
||||||
|
<template #header><i class="ti ti-messages" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.chat }}</template>
|
||||||
|
|
||||||
|
<div style="padding: 8px;">
|
||||||
|
<MkChatHistories/>
|
||||||
|
</div>
|
||||||
|
</XColumn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { i18n } from '../../i18n.js';
|
||||||
|
import XColumn from './column.vue';
|
||||||
|
import type { Column } from '@/deck.js';
|
||||||
|
import MkChatHistories from '@/components/MkChatHistories.vue';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
column: Column;
|
||||||
|
isStacked: boolean;
|
||||||
|
}>();
|
||||||
|
</script>
|
Loading…
Reference in New Issue