summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-04-15 15:36:53 +0900
committersyuilo <4439005+syuilo@users.noreply.github.com>2025-04-15 15:36:53 +0900
commit7c0806f208d85e29b9fc99f86844349e3708aa2d (patch)
tree113f07187890e805ae3d8f5cf5e6ad79df9b6e3a /packages/frontend/src
parentenhance(backend): フォローしているユーザーならフォロワー... (diff)
downloadsharkey-7c0806f208d85e29b9fc99f86844349e3708aa2d.tar.gz
sharkey-7c0806f208d85e29b9fc99f86844349e3708aa2d.tar.bz2
sharkey-7c0806f208d85e29b9fc99f86844349e3708aa2d.zip
feat(frontend): chat column
Resolve #15830
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkChatHistories.vue208
-rw-r--r--packages/frontend/src/deck.ts1
-rw-r--r--packages/frontend/src/pages/chat/home.home.vue161
-rw-r--r--packages/frontend/src/ui/deck.vue2
-rw-r--r--packages/frontend/src/ui/deck/chat-column.vue27
5 files changed, 241 insertions, 158 deletions
diff --git a/packages/frontend/src/components/MkChatHistories.vue b/packages/frontend/src/components/MkChatHistories.vue
new file mode 100644
index 0000000000..c508ea8451
--- /dev/null
+++ b/packages/frontend/src/components/MkChatHistories.vue
@@ -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>
diff --git a/packages/frontend/src/deck.ts b/packages/frontend/src/deck.ts
index 9df56c52df..c108a365b6 100644
--- a/packages/frontend/src/deck.ts
+++ b/packages/frontend/src/deck.ts
@@ -38,6 +38,7 @@ export const columnTypes = [
'mentions',
'direct',
'roleTimeline',
+ 'chat',
] as const;
export type ColumnType = typeof columnTypes[number];
diff --git a/packages/frontend/src/pages/chat/home.home.vue b/packages/frontend/src/pages/chat/home.home.vue
index a8ed891de0..a0853fb0c9 100644
--- a/packages/frontend/src/pages/chat/home.home.vue
+++ b/packages/frontend/src/pages/chat/home.home.vue
@@ -34,34 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFoldableSection>
<template #header>{{ i18n.ts._chat.history }}</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"/>
+ <MkChatHistories/>
</MkFoldableSection>
</div>
</template>
@@ -81,20 +54,12 @@ import { updateCurrentAccountPartial } from '@/accounts.js';
import MkInput from '@/components/MkInput.vue';
import MkFoldableSection from '@/components/MkFoldableSection.vue';
import MkInfo from '@/components/MkInfo.vue';
+import MkChatHistories from '@/components/MkChatHistories.vue';
const $i = ensureSignin();
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 searched = ref(false);
const searchResults = ref<Misskey.entities.ChatMessage[]>([]);
@@ -148,57 +113,8 @@ async function search() {
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(() => {
- fetchHistory();
+ updateCurrentAccountPartial({ hasUnreadChatMessages: false });
});
</script>
@@ -207,77 +123,6 @@ onMounted(() => {
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 {
padding: 12px;
border: solid 1px var(--MI_THEME-divider);
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 96961d951f..7556f513c2 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -97,6 +97,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue';
import XMentionsColumn from '@/ui/deck/mentions-column.vue';
import XDirectColumn from '@/ui/deck/direct-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 { columns, layout, columnTypes, switchProfileMenu, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
@@ -114,6 +115,7 @@ const columnComponents = {
mentions: XMentionsColumn,
direct: XDirectColumn,
roleTimeline: XRoleTimelineColumn,
+ chat: XChatColumn,
};
mainRouter.navHook = (path, flag): boolean => {
diff --git a/packages/frontend/src/ui/deck/chat-column.vue b/packages/frontend/src/ui/deck/chat-column.vue
new file mode 100644
index 0000000000..791af2e44c
--- /dev/null
+++ b/packages/frontend/src/ui/deck/chat-column.vue
@@ -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>