Merge branch 'misskey-develop' into merge/2025-03-24
# Conflicts: # package.json # packages/backend/src/core/AccountMoveService.ts # packages/frontend/src/components/MkDateSeparatedList.vue # packages/misskey-js/etc/misskey-js.api.md # pnpm-lock.yaml
This commit is contained in:
commit
3eeb53ff63
74 changed files with 622 additions and 242 deletions
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RoleCopyOnMoveAccount1743558299182 {
|
||||
name = 'RoleCopyOnMoveAccount1743558299182'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" ADD "preserveAssignmentOnMoveAccount" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "preserveAssignmentOnMoveAccount"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
|||
import InstanceChart from '@/core/chart/charts/instance.js';
|
||||
import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js';
|
||||
import { SystemAccountService } from '@/core/SystemAccountService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
|
||||
@Injectable()
|
||||
export class AccountMoveService {
|
||||
|
|
@ -64,6 +65,7 @@ export class AccountMoveService {
|
|||
private relayService: RelayService,
|
||||
private queueService: QueueService,
|
||||
private systemAccountService: SystemAccountService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -123,6 +125,7 @@ export class AccountMoveService {
|
|||
this.copyBlocking(src, dst),
|
||||
this.copyMutings(src, dst),
|
||||
this.deleteScheduledNotes(src),
|
||||
this.copyRoles(src, dst),
|
||||
this.updateLists(src, dst),
|
||||
]);
|
||||
} catch {
|
||||
|
|
@ -220,6 +223,32 @@ export class AccountMoveService {
|
|||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async copyRoles(src: ThinUser, dst: ThinUser): Promise<void> {
|
||||
// Insert new roles with the same values except userId
|
||||
// role service may have cache for roles so retrieve roles from service
|
||||
const [oldRoleAssignments, roles] = await Promise.all([
|
||||
this.roleService.getUserAssigns(src.id),
|
||||
this.roleService.getRoles(),
|
||||
]);
|
||||
|
||||
if (oldRoleAssignments.length === 0) return;
|
||||
|
||||
// No promise all since the only async operation is writing to the database
|
||||
for (const oldRoleAssignment of oldRoleAssignments) {
|
||||
const role = roles.find(x => x.id === oldRoleAssignment.roleId);
|
||||
if (role == null) continue; // Very unlikely however removing role may cause this case
|
||||
if (!role.preserveAssignmentOnMoveAccount) continue;
|
||||
|
||||
try {
|
||||
await this.roleService.assign(dst.id, role.id, oldRoleAssignment.expiresAt);
|
||||
} catch (e) {
|
||||
if (e instanceof RoleService.AlreadyAssignedError) continue;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update lists while moving accounts.
|
||||
* - No removal of the old account from the lists
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export class ChatService {
|
|||
text?: string | null;
|
||||
file?: MiDriveFile | null;
|
||||
uri?: string | null;
|
||||
}): Promise<Packed<'ChatMessageLite'>> {
|
||||
}): Promise<Packed<'ChatMessageLiteFor1on1'>> {
|
||||
if (fromUser.id === toUser.id) {
|
||||
throw new Error('yourself');
|
||||
}
|
||||
|
|
@ -210,7 +210,7 @@ export class ChatService {
|
|||
text?: string | null;
|
||||
file?: MiDriveFile | null;
|
||||
uri?: string | null;
|
||||
}): Promise<Packed<'ChatMessageLite'>> {
|
||||
}): Promise<Packed<'ChatMessageLiteForRoom'>> {
|
||||
const memberships = (await this.chatRoomMembershipsRepository.findBy({ roomId: toRoom.id })).map(m => ({
|
||||
userId: m.userId,
|
||||
isMuted: m.isMuted,
|
||||
|
|
|
|||
|
|
@ -639,6 +639,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
isModerator: values.isModerator,
|
||||
isExplorable: values.isExplorable,
|
||||
asBadge: values.asBadge,
|
||||
preserveAssignmentOnMoveAccount: values.preserveAssignmentOnMoveAccount,
|
||||
canEditMembersByModerator: values.canEditMembersByModerator,
|
||||
displayOrder: values.displayOrder,
|
||||
policies: values.policies,
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export class ChatEntityService {
|
|||
packedFiles: Map<MiChatMessage['fileId'], Packed<'DriveFile'> | null>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatMessageLite'>> {
|
||||
): Promise<Packed<'ChatMessageLiteFor1on1'>> {
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
|
||||
const message = typeof src === 'object' ? src : await this.chatMessagesRepository.findOneByOrFail({ id: src });
|
||||
|
|
@ -147,7 +147,7 @@ export class ChatEntityService {
|
|||
createdAt: this.idService.parse(message.id).date.toISOString(),
|
||||
text: message.text,
|
||||
fromUserId: message.fromUserId,
|
||||
toUserId: message.toUserId,
|
||||
toUserId: message.toUserId!,
|
||||
fileId: message.fileId,
|
||||
file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null,
|
||||
reactions,
|
||||
|
|
@ -177,7 +177,7 @@ export class ChatEntityService {
|
|||
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
|
||||
};
|
||||
},
|
||||
): Promise<Packed<'ChatMessageLite'>> {
|
||||
): Promise<Packed<'ChatMessageLiteForRoom'>> {
|
||||
const packedFiles = options?._hint_?.packedFiles;
|
||||
const packedUsers = options?._hint_?.packedUsers;
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ export class ChatEntityService {
|
|||
text: message.text,
|
||||
fromUserId: message.fromUserId,
|
||||
fromUser: packedUsers?.get(message.fromUserId) ?? await this.userEntityService.pack(message.fromUser ?? message.fromUserId),
|
||||
toRoomId: message.toRoomId,
|
||||
toRoomId: message.toRoomId!,
|
||||
fileId: message.fileId,
|
||||
file: message.fileId ? (packedFiles?.get(message.fileId) ?? await this.driveFileEntityService.pack(message.file ?? message.fileId)) : null,
|
||||
reactions,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import type { MiRole } from '@/models/Role.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
@Injectable()
|
||||
export class RoleEntityService {
|
||||
|
|
@ -31,7 +32,7 @@ export class RoleEntityService {
|
|||
public async pack(
|
||||
src: MiRole['id'] | MiRole,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
) {
|
||||
): Promise<Packed<'Role'>> {
|
||||
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
const assignedCount = await this.roleAssignmentsRepository.createQueryBuilder('assign')
|
||||
|
|
@ -67,6 +68,7 @@ export class RoleEntityService {
|
|||
isModerator: role.isModerator,
|
||||
isExplorable: role.isExplorable,
|
||||
asBadge: role.asBadge,
|
||||
preserveAssignmentOnMoveAccount: role.preserveAssignmentOnMoveAccount,
|
||||
canEditMembersByModerator: role.canEditMembersByModerator,
|
||||
displayOrder: role.displayOrder,
|
||||
policies: policies,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ import {
|
|||
} from '@/models/json-schema/meta.js';
|
||||
import { packedSystemWebhookSchema } from '@/models/json-schema/system-webhook.js';
|
||||
import { packedAbuseReportNotificationRecipientSchema } from '@/models/json-schema/abuse-report-notification-recipient.js';
|
||||
import { packedChatMessageSchema, packedChatMessageLiteSchema } from '@/models/json-schema/chat-message.js';
|
||||
import { packedChatMessageSchema, packedChatMessageLiteSchema, packedChatMessageLiteForRoomSchema, packedChatMessageLiteFor1on1Schema } from '@/models/json-schema/chat-message.js';
|
||||
import { packedChatRoomSchema } from '@/models/json-schema/chat-room.js';
|
||||
import { packedChatRoomInvitationSchema } from '@/models/json-schema/chat-room-invitation.js';
|
||||
import { packedChatRoomMembershipSchema } from '@/models/json-schema/chat-room-membership.js';
|
||||
|
|
@ -126,6 +126,8 @@ export const refs = {
|
|||
AbuseReportNotificationRecipient: packedAbuseReportNotificationRecipientSchema,
|
||||
ChatMessage: packedChatMessageSchema,
|
||||
ChatMessageLite: packedChatMessageLiteSchema,
|
||||
ChatMessageLiteFor1on1: packedChatMessageLiteFor1on1Schema,
|
||||
ChatMessageLiteForRoom: packedChatMessageLiteForRoomSchema,
|
||||
ChatRoom: packedChatRoomSchema,
|
||||
ChatRoomInvitation: packedChatRoomInvitationSchema,
|
||||
ChatRoomMembership: packedChatRoomMembershipSchema,
|
||||
|
|
|
|||
|
|
@ -248,6 +248,11 @@ export class MiRole {
|
|||
})
|
||||
public isExplorable: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public preserveAssignmentOnMoveAccount: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const packedChatMessageSchema = {
|
|||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
},
|
||||
|
|
@ -144,3 +144,113 @@ export const packedChatMessageLiteSchema = {
|
|||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedChatMessageLiteFor1on1Schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
toUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
reactions: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
reaction: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const packedChatMessageLiteForRoomSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUserId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
fromUser: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
toRoomId: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
fileId: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
file: {
|
||||
type: 'object',
|
||||
optional: true, nullable: true,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
reactions: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
reaction: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
user: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -397,6 +397,11 @@ export const packedRoleSchema = {
|
|||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
preserveAssignmentOnMoveAccount: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
canEditMembersByModerator: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const paramDef = {
|
|||
isAdministrator: { type: 'boolean' },
|
||||
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
|
||||
asBadge: { type: 'boolean' },
|
||||
preserveAssignmentOnMoveAccount: { type: 'boolean' },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
displayOrder: { type: 'number' },
|
||||
policies: {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const paramDef = {
|
|||
isAdministrator: { type: 'boolean' },
|
||||
isExplorable: { type: 'boolean' },
|
||||
asBadge: { type: 'boolean' },
|
||||
preserveAssignmentOnMoveAccount: { type: 'boolean' },
|
||||
canEditMembersByModerator: { type: 'boolean' },
|
||||
displayOrder: { type: 'number' },
|
||||
policies: {
|
||||
|
|
@ -78,6 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
isAdministrator: ps.isAdministrator,
|
||||
isExplorable: ps.isExplorable,
|
||||
asBadge: ps.asBadge,
|
||||
preserveAssignmentOnMoveAccount: ps.preserveAssignmentOnMoveAccount,
|
||||
canEditMembersByModerator: ps.canEditMembersByModerator,
|
||||
displayOrder: ps.displayOrder,
|
||||
policies: ps.policies,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const meta = {
|
|||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ChatMessageLite',
|
||||
ref: 'ChatMessageLiteForRoom',
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export const meta = {
|
|||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ChatMessageLite',
|
||||
ref: 'ChatMessageLiteFor1on1',
|
||||
},
|
||||
|
||||
errors: {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const meta = {
|
|||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const meta = {
|
|||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export const meta = {
|
|||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ChatMessageLite',
|
||||
ref: 'ChatMessageLiteForRoom',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export const meta = {
|
|||
tags: ['chat'],
|
||||
|
||||
requireCredential: true,
|
||||
requiredRolePolicy: 'canChat',
|
||||
|
||||
kind: 'write:chat',
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const meta = {
|
|||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
ref: 'ChatMessageLite',
|
||||
ref: 'ChatMessageLiteFor1on1',
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class BubbleTimelineChannel extends Channel {
|
|||
if (note.channelId != null) return;
|
||||
if (note.user.host == null) return;
|
||||
if (!this.instance.bubbleInstances.includes(note.user.host)) return;
|
||||
if (note.user.requireSigninToViewContents && this.user == null) return;
|
||||
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ class GlobalTimelineChannel extends Channel {
|
|||
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
if (note.user.requireSigninToViewContents && this.user == null) return;
|
||||
|
||||
if (isRenotePacked(note) && !isQuotePacked(note) && !this.withRenotes) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class LocalTimelineChannel extends Channel {
|
|||
if (note.user.host !== null) return;
|
||||
if (note.visibility !== 'public') return;
|
||||
if (note.channelId != null) return;
|
||||
if (note.user.requireSigninToViewContents && this.user == null) return;
|
||||
|
||||
// 関係ない返信は除外
|
||||
if (note.reply && this.user && !this.following[note.userId]?.withReplies && !this.withReplies) {
|
||||
|
|
|
|||
|
|
@ -13,18 +13,18 @@
|
|||
fgHighlighted: '#6bc9a0',
|
||||
fgOnWhite: '@accent',
|
||||
divider: '#cfcfcf',
|
||||
panel: '@X14',
|
||||
panel: '#ebe7e5',
|
||||
panelHeaderBg: '@panel',
|
||||
panelHeaderDivider: '@divider',
|
||||
header: ':alpha<0.7<@panel',
|
||||
navBg: '@X14',
|
||||
navBg: '#ebe7e5',
|
||||
renote: '#229e92',
|
||||
mention: '#da6d35',
|
||||
mentionMe: '#d44c4c',
|
||||
hashtag: '#4cb8d4',
|
||||
link: '@accent',
|
||||
buttonGradateB: ':hue<-70<@accent',
|
||||
success: '#86b300',
|
||||
X14: '#ebe7e5'
|
||||
success: '@accent',
|
||||
error: '#da5635',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,5 +18,8 @@
|
|||
mention: '@accent',
|
||||
mentionMe: 'rgb(170, 149, 98)',
|
||||
hashtag: '@accent',
|
||||
error: '#db9184',
|
||||
warn: '#dbc184',
|
||||
success: '#a3c975',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ queueMicrotask(() => {
|
|||
widgets(app);
|
||||
misskeyOS = os;
|
||||
if (isChromatic()) {
|
||||
prefer.set('animation', false);
|
||||
prefer.commit('animation', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -216,6 +216,14 @@ onUnmounted(() => {
|
|||
.content {
|
||||
--MI-stickyTop: 0px;
|
||||
|
||||
/*
|
||||
理屈は知らないけど、ここでbackgroundを設定しておかないと
|
||||
スクロールコンテナーが少なくともChromeにおいて
|
||||
main thread scrolling になってしまい、パフォーマンスが(多分)落ちる。
|
||||
backgroundが透明だと裏側を描画しないといけなくなるとかそういう理由かもしれない
|
||||
*/
|
||||
background: var(--MI_THEME-panel);
|
||||
|
||||
&.omitted {
|
||||
position: relative;
|
||||
max-height: var(--maxHeight);
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@ SPDX-FileCopyrightText: syuilo and misskey-project
|
|||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<!-- TODO: 親からスタイルを当てにくいことや実装がトリッキーなことを鑑み廃止または使用の縮小(timeline-date-separate.tsを使う) -->
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import type { MisskeyEntity } from '@/types/date-separated-list.js';
|
||||
import MkAd from '@/components/global/MkAd.vue';
|
||||
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import * as os from '@/os.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { getDateText } from '@/utility/timeline-date-separate.js';
|
||||
import { $i } from '@/i.js';
|
||||
|
||||
export default defineComponent({
|
||||
|
|
@ -46,15 +48,6 @@ export default defineComponent({
|
|||
setup(props, { slots, expose }) {
|
||||
const $style = useCssModule(); // カスタムレンダラなので使っても大丈夫
|
||||
|
||||
function getDateText(dateInstance: Date) {
|
||||
const date = dateInstance.getDate();
|
||||
const month = dateInstance.getMonth() + 1;
|
||||
return i18n.tsx.monthAndDay({
|
||||
month: month.toString(),
|
||||
day: date.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
if (props.items.length === 0) return;
|
||||
|
||||
const renderChildrenImpl = (shouldHideAds: boolean) => props.items.map((item, i) => {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<MkSwitch v-model="role.preserveAssignmentOnMoveAccount" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.preserveAssignmentOnMoveAccount }}</template>
|
||||
<template #caption>{{ i18n.ts._role.preserveAssignmentOnMoveAccount_description }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="role.canEditMembersByModerator" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
|
||||
|
|
|
|||
|
|
@ -5,33 +5,28 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div :class="[$style.root, { [$style.isMe]: isMe }]">
|
||||
<MkAvatar :class="$style.avatar" :user="message.fromUser" :link="!isMe" :preview="false"/>
|
||||
<MkAvatar :class="$style.avatar" :user="message.fromUser!" :link="!isMe" :preview="false"/>
|
||||
<div :class="$style.body" @contextmenu.stop="onContextmenu">
|
||||
<div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName']" :user="message.fromUser"/></div>
|
||||
<div :class="$style.header"><MkUserName v-if="!isMe && prefer.s['chat.showSenderName'] && message.fromUser != null" :user="message.fromUser"/></div>
|
||||
<MkFukidashi :class="$style.fukidashi" :tail="isMe ? 'right' : 'left'" :accented="isMe">
|
||||
<div v-if="!message.isDeleted" :class="$style.content">
|
||||
<Mfm
|
||||
v-if="message.text"
|
||||
ref="text"
|
||||
class="_selectable"
|
||||
:text="message.text"
|
||||
:i="$i"
|
||||
:nyaize="'respect'"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
|
||||
</div>
|
||||
<div v-else :class="$style.content">
|
||||
<p>{{ i18n.ts.deleted }}</p>
|
||||
</div>
|
||||
<Mfm
|
||||
v-if="message.text"
|
||||
ref="text"
|
||||
class="_selectable"
|
||||
:text="message.text"
|
||||
:i="$i"
|
||||
:nyaize="'respect'"
|
||||
:enableEmojiMenu="true"
|
||||
:enableEmojiMenuReaction="true"
|
||||
/>
|
||||
<MkMediaList v-if="message.file" :mediaList="[message.file]" :class="$style.file"/>
|
||||
</MkFukidashi>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" style="margin: 8px 0;"/>
|
||||
<div :class="$style.footer">
|
||||
<button class="_textButton" style="color: currentColor;" @click="showMenu"><i class="ti ti-dots-circle-horizontal"></i></button>
|
||||
<MkTime :class="$style.time" :time="message.createdAt"/>
|
||||
<MkA v-if="isSearchResult && message.toRoomId" :to="`/chat/room/${message.toRoomId}`">{{ message.toRoom.name }}</MkA>
|
||||
<MkA v-if="isSearchResult && message.toUserId && isMe" :to="`/chat/user/${message.toUserId}`">@{{ message.toUser.username }}</MkA>
|
||||
<MkA v-if="isSearchResult && 'toRoom' in message && message.toRoom != null" :to="`/chat/room/${message.toRoomId}`">{{ message.toRoom.name }}</MkA>
|
||||
<MkA v-if="isSearchResult && 'toUser' in message && message.toUser != null && isMe" :to="`/chat/user/${message.toUserId}`">@{{ message.toUser.username }}</MkA>
|
||||
</div>
|
||||
<TransitionGroup
|
||||
:enterActiveClass="prefer.s.animation ? $style.transition_reaction_enterActive : ''"
|
||||
|
|
@ -62,6 +57,7 @@ import * as Misskey from 'misskey-js';
|
|||
import { url } from '@@/js/config.js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { NormalizedChatMessage } from './room.vue';
|
||||
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
|
|
@ -76,11 +72,12 @@ import * as sound from '@/utility/sound.js';
|
|||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const props = defineProps<{
|
||||
message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage;
|
||||
message: NormalizedChatMessage | Misskey.entities.ChatMessage;
|
||||
isSearchResult?: boolean;
|
||||
}>();
|
||||
|
||||
|
|
@ -88,6 +85,8 @@ const isMe = computed(() => props.message.fromUserId === $i.id);
|
|||
const urls = computed(() => props.message.text ? extractUrlFromMfm(mfm.parse(props.message.text)) : []);
|
||||
|
||||
provide(DI.mfmEmojiReactCallback, (reaction) => {
|
||||
if (!$i.policies.canChat) return;
|
||||
|
||||
sound.playMisskeySfx('reaction');
|
||||
misskeyApi('chat/messages/react', {
|
||||
messageId: props.message.id,
|
||||
|
|
@ -96,7 +95,12 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
|
|||
});
|
||||
|
||||
function react(ev: MouseEvent) {
|
||||
reactionPicker.show(ev.currentTarget ?? ev.target, null, async (reaction) => {
|
||||
if (!$i.policies.canChat) return;
|
||||
|
||||
const targetEl = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
|
||||
if (!targetEl) return;
|
||||
|
||||
reactionPicker.show(targetEl, null, async (reaction) => {
|
||||
sound.playMisskeySfx('reaction');
|
||||
misskeyApi('chat/messages/react', {
|
||||
messageId: props.message.id,
|
||||
|
|
@ -106,6 +110,8 @@ function react(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
function onReactionClick(record: Misskey.entities.ChatMessage['reactions'][0]) {
|
||||
if (!$i.policies.canChat) return;
|
||||
|
||||
if (record.user.id === $i.id) {
|
||||
misskeyApi('chat/messages/unreact', {
|
||||
messageId: props.message.id,
|
||||
|
|
@ -132,7 +138,7 @@ function onContextmenu(ev: MouseEvent) {
|
|||
function showMenu(ev: MouseEvent, contextmenu = false) {
|
||||
const menu: MenuItem[] = [];
|
||||
|
||||
if (!isMe.value) {
|
||||
if (!isMe.value && $i.policies.canChat) {
|
||||
menu.push({
|
||||
text: i18n.ts.reaction,
|
||||
icon: 'ti ti-mood-plus',
|
||||
|
|
@ -150,7 +156,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
|
|||
text: i18n.ts.copyContent,
|
||||
icon: 'ti ti-copy',
|
||||
action: () => {
|
||||
copyToClipboard(props.message.text);
|
||||
copyToClipboard(props.message.text ?? '');
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -158,7 +164,7 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
|
|||
type: 'divider',
|
||||
});
|
||||
|
||||
if (isMe.value) {
|
||||
if (isMe.value && $i.policies.canChat) {
|
||||
menu.push({
|
||||
text: i18n.ts.delete,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
@ -169,14 +175,16 @@ function showMenu(ev: MouseEvent, contextmenu = false) {
|
|||
});
|
||||
},
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!isMe.value && props.message.fromUser != null) {
|
||||
menu.push({
|
||||
text: i18n.ts.reportAbuse,
|
||||
icon: 'ti ti-exclamation-circle',
|
||||
action: () => {
|
||||
const localUrl = `${url}/chat/messages/${props.message.id}`;
|
||||
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
|
||||
user: props.message.fromUser,
|
||||
user: props.message.fromUser!,
|
||||
initialComment: `${localUrl}\n-----\n`,
|
||||
}, {
|
||||
closed: () => dispose(),
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onActivated, onDeactivated, onMounted, ref } from 'vue';
|
||||
import { onActivated, onDeactivated, onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { useInterval } from '@@/js/use-interval.js';
|
||||
import XMessage from './XMessage.vue';
|
||||
|
|
@ -163,7 +163,7 @@ async function fetchHistory() {
|
|||
.map(m => ({
|
||||
id: m.id,
|
||||
message: m,
|
||||
other: m.room == null ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
|
||||
other: (!('room' in m) || m.room == null) ? (m.fromUserId === $i.id ? m.toUser : m.fromUser) : null,
|
||||
isMe: m.fromUserId === $i.id,
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -35,18 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const fetching = ref(true);
|
||||
|
|
@ -55,8 +51,7 @@ const invitations = ref<Misskey.entities.ChatRoomInvitation[]>([]);
|
|||
async function fetchInvitations() {
|
||||
fetching.value = true;
|
||||
|
||||
const res = await misskeyApi('chat/rooms/invitations/inbox', {
|
||||
});
|
||||
const res = await misskeyApi('chat/rooms/invitations/inbox');
|
||||
|
||||
invitations.value = res;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<div class="_gaps">
|
||||
<div v-if="memberships.length > 0" class="_gaps_s">
|
||||
<XRoom v-for="membership in memberships" :key="membership.id" :room="membership.room"/>
|
||||
<XRoom v-for="membership in memberships" :key="membership.id" :room="membership.room!"/>
|
||||
</div>
|
||||
<div v-if="!fetching && memberships.length == 0" class="_fullinfo">
|
||||
<div>{{ i18n.ts._chat.noRooms }}</div>
|
||||
|
|
@ -16,19 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XRoom from './XRoom.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const fetching = ref(true);
|
||||
const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
|
||||
|
|
@ -36,8 +28,7 @@ const memberships = ref<Misskey.entities.ChatRoomMembership[]>([]);
|
|||
async function fetchRooms() {
|
||||
fetching.value = true;
|
||||
|
||||
const res = await misskeyApi('chat/rooms/joining', {
|
||||
});
|
||||
const res = await misskeyApi('chat/rooms/joining');
|
||||
|
||||
memberships.value = res;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,19 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XRoom from './XRoom.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { useRouter } from '@/router.js';
|
||||
import * as os from '@/os.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const fetching = ref(true);
|
||||
const rooms = ref<Misskey.entities.ChatRoom[]>([]);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import XHome from './home.home.vue';
|
||||
import XInvitations from './home.invitations.vue';
|
||||
import XJoiningRooms from './home.joiningRooms.vue';
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<PageWithHeader>
|
||||
<MkSpacer :contentMax="700">
|
||||
<div v-if="initializing">
|
||||
<div v-if="initializing || message == null">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
@ -17,23 +17,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XMessage from './XMessage.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
messageId?: string;
|
||||
}>();
|
||||
|
||||
const initializing = ref(true);
|
||||
const message = ref<Misskey.entities.ChatMessage>();
|
||||
const message = ref<Misskey.entities.ChatMessage | null>();
|
||||
|
||||
async function initialize() {
|
||||
initializing.value = true;
|
||||
|
|
|
|||
|
|
@ -34,14 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, watch, ref, shallowRef, computed, nextTick, readonly } from 'vue';
|
||||
import { onMounted, watch, ref, shallowRef, computed, nextTick, readonly, onBeforeUnmount } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
//import insertTextAtCursor from 'insert-text-at-cursor';
|
||||
import { throttle } from 'throttle-debounce';
|
||||
import { formatTimeString } from '@/utility/format-time-string.js';
|
||||
import { selectFile } from '@/utility/select-file.js';
|
||||
import * as os from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { uploadFile } from '@/utility/upload.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
|
|
@ -62,6 +60,7 @@ const text = ref<string>('');
|
|||
const file = ref<Misskey.entities.DriveFile | null>(null);
|
||||
const sending = ref(false);
|
||||
const textareaReadOnly = ref(false);
|
||||
let autocompleteInstance: Autocomplete | null = null;
|
||||
|
||||
const canSend = computed(() => (text.value != null && text.value !== '') || file.value != null);
|
||||
|
||||
|
|
@ -171,7 +170,9 @@ function chooseFile(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
function onChangeFile() {
|
||||
if (fileEl.value.files![0]) upload(fileEl.value.files[0]);
|
||||
if (fileEl.value == null || fileEl.value.files == null) return;
|
||||
|
||||
if (fileEl.value.files[0]) upload(fileEl.value.files[0]);
|
||||
}
|
||||
|
||||
function upload(fileToUpload: File, name?: string) {
|
||||
|
|
@ -270,8 +271,9 @@ async function insertEmoji(ev: MouseEvent) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
// TODO: detach when unmount
|
||||
new Autocomplete(textareaEl.value, text);
|
||||
if (textareaEl.value != null) {
|
||||
autocompleteInstance = new Autocomplete(textareaEl.value, text);
|
||||
}
|
||||
|
||||
// 書きかけの投稿を復元
|
||||
const draft = JSON.parse(miLocalStorage.getItem('chatMessageDrafts') || '{}')[getDraftKey()];
|
||||
|
|
@ -280,6 +282,13 @@ onMounted(() => {
|
|||
file.value = draft.data.file;
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (autocompleteInstance) {
|
||||
autocompleteInstance.detach();
|
||||
autocompleteInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -26,11 +26,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -73,7 +72,7 @@ async function del() {
|
|||
router.push('/chat');
|
||||
}
|
||||
|
||||
const isMuted = ref(props.room.isMuted);
|
||||
const isMuted = ref(props.room.isMuted ?? false);
|
||||
|
||||
watch(isMuted, async () => {
|
||||
await os.apiWithDialog('chat/rooms/mute', {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<hr v-if="memberships.length > 0">
|
||||
|
||||
<div v-for="membership in memberships" :key="membership.id" :class="$style.membership">
|
||||
<MkA :class="$style.membershipBody" :to="`${userPage(membership.user)}`">
|
||||
<MkUserCardMini :user="membership.user"/>
|
||||
<MkA :class="$style.membershipBody" :to="`${userPage(membership.user!)}`">
|
||||
<MkUserCardMini :user="membership.user!"/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
||||
|
|
@ -39,7 +39,6 @@ import * as Misskey from 'misskey-js';
|
|||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
|
|
|
|||
|
|
@ -33,14 +33,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XMessage from './XMessage.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import * as os from '@/os.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
|
||||
tag="div" class="_gaps"
|
||||
>
|
||||
<XMessage v-for="message in messages.toReversed()" :key="message.id" :message="message"/>
|
||||
<template v-for="item in timeline.toReversed()" :key="item.id">
|
||||
<XMessage v-if="item.type === 'item'" :message="item.data"/>
|
||||
<div v-else-if="item.type === 'date'" :class="$style.dateDivider">
|
||||
<span><i class="ti ti-chevron-up"></i> {{ item.nextText }}</span>
|
||||
<span style="height: 1em; width: 1px; background: var(--MI_THEME-divider);"></span>
|
||||
<span>{{ item.prevText }} <i class="ti ti-chevron-down"></i></span>
|
||||
</div>
|
||||
</template>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
|
|
@ -79,15 +86,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, useTemplateRef, computed, watch, onMounted, nextTick, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
|
||||
import { ref, useTemplateRef, computed, onMounted, onBeforeUnmount, onDeactivated, onActivated } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { getScrollContainer, isTailVisible } from '@@/js/scroll.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import XMessage from './XMessage.vue';
|
||||
import XForm from './room.form.vue';
|
||||
import XSearch from './room.search.vue';
|
||||
import XMembers from './room.members.vue';
|
||||
import XInfo from './room.info.vue';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
import type { PageHeaderItem } from '@/types/page-header.js';
|
||||
import * as os from '@/os.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
|
|
@ -100,6 +108,7 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import { useRouter } from '@/router.js';
|
||||
import { useMutationObserver } from '@/use/use-mutation-observer.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { makeDateSeparatedTimelineComputedRef } from '@/utility/timeline-date-separate.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
const router = useRouter();
|
||||
|
|
@ -109,15 +118,23 @@ const props = defineProps<{
|
|||
roomId?: string;
|
||||
}>();
|
||||
|
||||
export type NormalizedChatMessage = Omit<Misskey.entities.ChatMessageLite, 'fromUser' | 'reactions'> & {
|
||||
fromUser: Misskey.entities.UserLite;
|
||||
reactions: (Misskey.entities.ChatMessageLite['reactions'][number] & {
|
||||
user: Misskey.entities.UserLite;
|
||||
})[];
|
||||
};
|
||||
|
||||
const initializing = ref(true);
|
||||
const moreFetching = ref(false);
|
||||
const messages = ref<Misskey.entities.ChatMessage[]>([]);
|
||||
const messages = ref<NormalizedChatMessage[]>([]);
|
||||
const canFetchMore = ref(false);
|
||||
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||
const room = ref<Misskey.entities.ChatRoom | null>(null);
|
||||
const connection = ref<Misskey.ChannelConnection<Misskey.Channels['chatUser'] | Misskey.Channels['chatRoom']> | null>(null);
|
||||
const connection = ref<Misskey.IChannelConnection<Misskey.Channels['chatUser']> | Misskey.IChannelConnection<Misskey.Channels['chatRoom']> | null>(null);
|
||||
const showIndicator = ref(false);
|
||||
const timelineEl = useTemplateRef('timelineEl');
|
||||
const timeline = makeDateSeparatedTimelineComputedRef(messages);
|
||||
|
||||
const SCROLL_HEAD_THRESHOLD = 200;
|
||||
|
||||
|
|
@ -138,18 +155,14 @@ useMutationObserver(timelineEl, {
|
|||
}
|
||||
});
|
||||
|
||||
function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage) {
|
||||
const reactions = [...message.reactions];
|
||||
for (const record of reactions) {
|
||||
if (room.value == null && record.user == null) { // 1on1の時はuserは省略される
|
||||
record.user = message.fromUserId === $i.id ? user.value : $i;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeMessage(message: Misskey.entities.ChatMessageLite | Misskey.entities.ChatMessage): NormalizedChatMessage {
|
||||
return {
|
||||
...message,
|
||||
fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user),
|
||||
reactions,
|
||||
fromUser: message.fromUser ?? (message.fromUserId === $i.id ? $i : user.value!),
|
||||
reactions: message.reactions.map(record => ({
|
||||
...record,
|
||||
user: record.user ?? (message.fromUserId === $i.id ? user.value! : $i),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -184,8 +197,8 @@ async function initialize() {
|
|||
misskeyApi('chat/messages/room-timeline', { roomId: props.roomId, limit: LIMIT }),
|
||||
]);
|
||||
|
||||
room.value = r;
|
||||
messages.value = m.map(x => normalizeMessage(x));
|
||||
room.value = r as Misskey.entities.ChatRoomsShowResponse;
|
||||
messages.value = (m as Misskey.entities.ChatMessagesRoomTimelineResponse).map(x => normalizeMessage(x));
|
||||
|
||||
if (messages.value.length === LIMIT) {
|
||||
canFetchMore.value = true;
|
||||
|
|
@ -221,11 +234,11 @@ async function fetchMore() {
|
|||
moreFetching.value = true;
|
||||
|
||||
const newMessages = props.userId ? await misskeyApi('chat/messages/user-timeline', {
|
||||
userId: user.value.id,
|
||||
userId: user.value!.id,
|
||||
limit: LIMIT,
|
||||
untilId: messages.value[messages.value.length - 1].id,
|
||||
}) : await misskeyApi('chat/messages/room-timeline', {
|
||||
roomId: room.value.id,
|
||||
roomId: room.value!.id,
|
||||
limit: LIMIT,
|
||||
untilId: messages.value[messages.value.length - 1].id,
|
||||
});
|
||||
|
|
@ -236,7 +249,7 @@ async function fetchMore() {
|
|||
moreFetching.value = false;
|
||||
}
|
||||
|
||||
function onMessage(message: Misskey.entities.ChatMessage) {
|
||||
function onMessage(message: Misskey.entities.ChatMessageLite) {
|
||||
sound.playMisskeySfx('chatMessage');
|
||||
|
||||
messages.value.unshift(normalizeMessage(message));
|
||||
|
|
@ -253,34 +266,34 @@ function onMessage(message: Misskey.entities.ChatMessage) {
|
|||
}
|
||||
}
|
||||
|
||||
function onDeleted(id) {
|
||||
function onDeleted(id: string) {
|
||||
const index = messages.value.findIndex(m => m.id === id);
|
||||
if (index !== -1) {
|
||||
messages.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function onReact(ctx) {
|
||||
function onReact(ctx: Parameters<Misskey.Channels['chatUser']['events']['react']>[0] | Parameters<Misskey.Channels['chatRoom']['events']['react']>[0]) {
|
||||
const message = messages.value.find(m => m.id === ctx.messageId);
|
||||
if (message) {
|
||||
if (room.value == null) { // 1on1の時はuserは省略される
|
||||
message.reactions.push({
|
||||
reaction: ctx.reaction,
|
||||
user: message.fromUserId === $i.id ? user : $i,
|
||||
user: message.fromUserId === $i.id ? user.value! : $i,
|
||||
});
|
||||
} else {
|
||||
message.reactions.push({
|
||||
reaction: ctx.reaction,
|
||||
user: ctx.user,
|
||||
user: ctx.user!,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onUnreact(ctx) {
|
||||
function onUnreact(ctx: Parameters<Misskey.Channels['chatUser']['events']['unreact']>[0] | Parameters<Misskey.Channels['chatRoom']['events']['unreact']>[0]) {
|
||||
const message = messages.value.find(m => m.id === ctx.messageId);
|
||||
if (message) {
|
||||
const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user.id);
|
||||
const index = message.reactions.findIndex(r => r.reaction === ctx.reaction && r.user.id === ctx.user!.id);
|
||||
if (index !== -1) {
|
||||
message.reactions.splice(index, 1);
|
||||
}
|
||||
|
|
@ -310,14 +323,18 @@ onBeforeUnmount(() => {
|
|||
});
|
||||
|
||||
async function inviteUser() {
|
||||
if (room.value == null) return;
|
||||
|
||||
const invitee = await os.selectUser({ includeSelf: false, localOnly: true });
|
||||
os.apiWithDialog('chat/rooms/invitations/create', {
|
||||
roomId: room.value?.id,
|
||||
roomId: room.value.id,
|
||||
userId: invitee.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function leaveRoom() {
|
||||
if (room.value == null) return;
|
||||
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.areYouSure,
|
||||
|
|
@ -325,7 +342,7 @@ async function leaveRoom() {
|
|||
if (canceled) return;
|
||||
|
||||
misskeyApi('chat/rooms/leave', {
|
||||
roomId: room.value?.id,
|
||||
roomId: room.value.id,
|
||||
});
|
||||
router.push('/chat');
|
||||
}
|
||||
|
|
@ -384,19 +401,36 @@ const headerTabs = computed(() => room.value ? [{
|
|||
icon: 'ti ti-search',
|
||||
}]);
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
const headerActions = computed<PageHeaderItem[]>(() => [{
|
||||
icon: 'ti ti-dots',
|
||||
text: '',
|
||||
handler: showMenu,
|
||||
}]);
|
||||
|
||||
definePage(computed(() => !initializing.value ? user.value ? {
|
||||
userName: user,
|
||||
title: user.value.name ?? user.value.username,
|
||||
avatar: user,
|
||||
} : {
|
||||
title: room.value?.name,
|
||||
icon: 'ti ti-users',
|
||||
} : null));
|
||||
definePage(computed(() => {
|
||||
if (!initializing.value) {
|
||||
if (user.value) {
|
||||
return {
|
||||
userName: user.value,
|
||||
title: user.value.name ?? user.value.username,
|
||||
avatar: user.value,
|
||||
};
|
||||
} else if (room.value) {
|
||||
return {
|
||||
title: room.value.name,
|
||||
icon: 'ti ti-users',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
title: i18n.ts.chat,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: i18n.ts.chat,
|
||||
};
|
||||
}
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
@ -464,4 +498,18 @@ definePage(computed(() => !initializing.value ? user.value ? {
|
|||
transition: opacity 0.5s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dateDivider {
|
||||
display: flex;
|
||||
font-size: 85%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5em;
|
||||
opacity: 0.75;
|
||||
border: solid 0.5px var(--MI_THEME-divider);
|
||||
border-radius: 999px;
|
||||
width: fit-content;
|
||||
padding: 0.5em 1em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@
|
|||
*/
|
||||
|
||||
import { onUnmounted, watch } from 'vue';
|
||||
import type { Ref, ShallowRef } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
export function useMutationObserver(targetNodeRef: Ref<HTMLElement | undefined>, options: MutationObserverInit, callback: MutationCallback): void {
|
||||
export function useMutationObserver(targetNodeRef: Ref<HTMLElement | null | undefined>, options: MutationObserverInit, callback: MutationCallback): void {
|
||||
const observer = new MutationObserver(callback);
|
||||
|
||||
watch(targetNodeRef, (targetNode) => {
|
||||
|
|
|
|||
63
packages/frontend/src/utility/timeline-date-separate.ts
Normal file
63
packages/frontend/src/utility/timeline-date-separate.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { computed } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
export function getDateText(dateInstance: Date) {
|
||||
const date = dateInstance.getDate();
|
||||
const month = dateInstance.getMonth() + 1;
|
||||
return `${month.toString()}/${date.toString()}`;
|
||||
}
|
||||
|
||||
export type DateSeparetedTimelineItem<T> = {
|
||||
id: string;
|
||||
type: 'item';
|
||||
data: T;
|
||||
} | {
|
||||
id: string;
|
||||
type: 'date';
|
||||
prev: Date;
|
||||
prevText: string;
|
||||
next: Date;
|
||||
nextText: string;
|
||||
};
|
||||
|
||||
export function makeDateSeparatedTimelineComputedRef<T extends { id: string; createdAt: string; }>(items: Ref<T[]>) {
|
||||
return computed<DateSeparetedTimelineItem<T>[]>(() => {
|
||||
const tl: DateSeparetedTimelineItem<T>[] = [];
|
||||
for (let i = 0; i < items.value.length; i++) {
|
||||
const item = items.value[i];
|
||||
|
||||
const date = new Date(item.createdAt);
|
||||
const nextDate = items.value[i + 1] ? new Date(items.value[i + 1].createdAt) : null;
|
||||
|
||||
tl.push({
|
||||
id: item.id,
|
||||
type: 'item',
|
||||
data: item,
|
||||
});
|
||||
|
||||
if (
|
||||
i !== items.value.length - 1 &&
|
||||
nextDate != null && (
|
||||
date.getFullYear() !== nextDate.getFullYear() ||
|
||||
date.getMonth() !== nextDate.getMonth() ||
|
||||
date.getDate() !== nextDate.getDate()
|
||||
)
|
||||
) {
|
||||
tl.push({
|
||||
id: `date-${item.id}`,
|
||||
type: 'date',
|
||||
prev: date,
|
||||
prevText: getDateText(date),
|
||||
next: nextDate,
|
||||
nextText: getDateText(nextDate),
|
||||
});
|
||||
}
|
||||
}
|
||||
return tl;
|
||||
});
|
||||
}
|
||||
|
|
@ -853,6 +853,54 @@ export type Channels = {
|
|||
claimTimeIsUp: null | Record<string, never>;
|
||||
};
|
||||
};
|
||||
chatUser: {
|
||||
params: {
|
||||
otherId: string;
|
||||
};
|
||||
events: {
|
||||
message: (payload: ChatMessageLite) => void;
|
||||
deleted: (payload: ChatMessageLite['id']) => void;
|
||||
react: (payload: {
|
||||
reaction: string;
|
||||
user?: UserLite;
|
||||
messageId: ChatMessageLite['id'];
|
||||
}) => void;
|
||||
unreact: (payload: {
|
||||
reaction: string;
|
||||
user?: UserLite;
|
||||
messageId: ChatMessageLite['id'];
|
||||
}) => void;
|
||||
};
|
||||
receives: {
|
||||
read: {
|
||||
id: ChatMessageLite['id'];
|
||||
};
|
||||
};
|
||||
};
|
||||
chatRoom: {
|
||||
params: {
|
||||
roomId: string;
|
||||
};
|
||||
events: {
|
||||
message: (payload: ChatMessageLite) => void;
|
||||
deleted: (payload: ChatMessageLite['id']) => void;
|
||||
react: (payload: {
|
||||
reaction: string;
|
||||
user?: UserLite;
|
||||
messageId: ChatMessageLite['id'];
|
||||
}) => void;
|
||||
unreact: (payload: {
|
||||
reaction: string;
|
||||
user?: UserLite;
|
||||
messageId: ChatMessageLite['id'];
|
||||
}) => void;
|
||||
};
|
||||
receives: {
|
||||
read: {
|
||||
id: ChatMessageLite['id'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
@ -999,6 +1047,12 @@ type ChatMessage = components['schemas']['ChatMessage'];
|
|||
// @public (undocumented)
|
||||
type ChatMessageLite = components['schemas']['ChatMessageLite'];
|
||||
|
||||
// @public (undocumented)
|
||||
type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1'];
|
||||
|
||||
// @public (undocumented)
|
||||
type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom'];
|
||||
|
||||
// @public (undocumented)
|
||||
type ChatMessagesCreateToRoomRequest = operations['chat___messages___create-to-room']['requestBody']['content']['application/json'];
|
||||
|
||||
|
|
@ -2152,6 +2206,8 @@ declare namespace entities {
|
|||
AbuseReportNotificationRecipient,
|
||||
ChatMessage,
|
||||
ChatMessageLite,
|
||||
ChatMessageLiteFor1on1,
|
||||
ChatMessageLiteForRoom,
|
||||
ChatRoom,
|
||||
ChatRoomInvitation,
|
||||
ChatRoomMembership
|
||||
|
|
@ -3853,8 +3909,8 @@ type V2AdminEmojiListResponse = operations['v2___admin___emoji___list']['respons
|
|||
//
|
||||
// src/entities.ts:50:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:233:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:243:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:234:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:244:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2025.4.0-beta.1",
|
||||
"version": "2025.4.0-rc.0",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ export type SystemWebhook = components['schemas']['SystemWebhook'];
|
|||
export type AbuseReportNotificationRecipient = components['schemas']['AbuseReportNotificationRecipient'];
|
||||
export type ChatMessage = components['schemas']['ChatMessage'];
|
||||
export type ChatMessageLite = components['schemas']['ChatMessageLite'];
|
||||
export type ChatMessageLiteFor1on1 = components['schemas']['ChatMessageLiteFor1on1'];
|
||||
export type ChatMessageLiteForRoom = components['schemas']['ChatMessageLiteForRoom'];
|
||||
export type ChatRoom = components['schemas']['ChatRoom'];
|
||||
export type ChatRoomInvitation = components['schemas']['ChatRoomInvitation'];
|
||||
export type ChatRoomMembership = components['schemas']['ChatRoomMembership'];
|
||||
|
|
|
|||
|
|
@ -5405,6 +5405,8 @@ export type components = {
|
|||
/** @example false */
|
||||
asBadge: boolean;
|
||||
/** @example false */
|
||||
preserveAssignmentOnMoveAccount: boolean;
|
||||
/** @example false */
|
||||
canEditMembersByModerator: boolean;
|
||||
policies: {
|
||||
[key: string]: {
|
||||
|
|
@ -5693,10 +5695,10 @@ export type components = {
|
|||
fileId?: string | null;
|
||||
file?: components['schemas']['DriveFile'] | null;
|
||||
isRead?: boolean;
|
||||
reactions: ({
|
||||
reactions: {
|
||||
reaction: string;
|
||||
user?: components['schemas']['UserLite'] | null;
|
||||
})[];
|
||||
user: components['schemas']['UserLite'];
|
||||
}[];
|
||||
};
|
||||
ChatMessageLite: {
|
||||
id: string;
|
||||
|
|
@ -5714,6 +5716,34 @@ export type components = {
|
|||
user?: components['schemas']['UserLite'] | null;
|
||||
})[];
|
||||
};
|
||||
ChatMessageLiteFor1on1: {
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
fromUserId: string;
|
||||
toUserId: string;
|
||||
text?: string | null;
|
||||
fileId?: string | null;
|
||||
file?: components['schemas']['DriveFile'] | null;
|
||||
reactions: {
|
||||
reaction: string;
|
||||
}[];
|
||||
};
|
||||
ChatMessageLiteForRoom: {
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
fromUserId: string;
|
||||
fromUser: components['schemas']['UserLite'];
|
||||
toRoomId: string;
|
||||
text?: string | null;
|
||||
fileId?: string | null;
|
||||
file?: components['schemas']['DriveFile'] | null;
|
||||
reactions: {
|
||||
reaction: string;
|
||||
user: components['schemas']['UserLite'];
|
||||
}[];
|
||||
};
|
||||
ChatRoom: {
|
||||
id: string;
|
||||
/** Format: date-time */
|
||||
|
|
@ -9983,6 +10013,7 @@ export type operations = {
|
|||
/** @default false */
|
||||
isExplorable?: boolean;
|
||||
asBadge: boolean;
|
||||
preserveAssignmentOnMoveAccount?: boolean;
|
||||
canEditMembersByModerator: boolean;
|
||||
displayOrder: number;
|
||||
policies: Record<string, never>;
|
||||
|
|
@ -10258,6 +10289,7 @@ export type operations = {
|
|||
isAdministrator?: boolean;
|
||||
isExplorable?: boolean;
|
||||
asBadge?: boolean;
|
||||
preserveAssignmentOnMoveAccount?: boolean;
|
||||
canEditMembersByModerator?: boolean;
|
||||
displayOrder?: number;
|
||||
policies?: Record<string, never>;
|
||||
|
|
@ -15108,7 +15140,7 @@ export type operations = {
|
|||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ChatMessageLite'];
|
||||
'application/json': components['schemas']['ChatMessageLiteForRoom'];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
|
|
@ -15171,7 +15203,7 @@ export type operations = {
|
|||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ChatMessageLite'];
|
||||
'application/json': components['schemas']['ChatMessageLiteFor1on1'];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
|
|
@ -15346,7 +15378,7 @@ export type operations = {
|
|||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ChatMessageLite'][];
|
||||
'application/json': components['schemas']['ChatMessageLiteForRoom'][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
|
|
@ -15574,7 +15606,7 @@ export type operations = {
|
|||
/** @description OK (with results) */
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ChatMessageLite'][];
|
||||
'application/json': components['schemas']['ChatMessageLiteFor1on1'][];
|
||||
};
|
||||
};
|
||||
/** @description Client error */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Antenna,
|
||||
ChatMessage,
|
||||
ChatMessageLite,
|
||||
DriveFile,
|
||||
DriveFolder,
|
||||
Note,
|
||||
|
|
@ -243,7 +244,55 @@ export type Channels = {
|
|||
updateSettings: ReversiUpdateSettings<ReversiUpdateKey>;
|
||||
claimTimeIsUp: null | Record<string, never>;
|
||||
}
|
||||
}
|
||||
};
|
||||
chatUser: {
|
||||
params: {
|
||||
otherId: string;
|
||||
};
|
||||
events: {
|
||||
message: (payload: ChatMessageLite) => void;
|
||||
deleted: (payload: ChatMessageLite['id']) => void;
|
||||
react: (payload: {
|
||||
reaction: string;
|
||||
user?: UserLite;
|
||||
messageId: ChatMessageLite['id'];
|
||||
}) => void;
|
||||
unreact: (payload: {
|
||||
reaction: string;
|
||||
user?: UserLite;
|
||||
messageId: ChatMessageLite['id'];
|
||||
}) => void;
|
||||
};
|
||||
receives: {
|
||||
read: {
|
||||
id: ChatMessageLite['id'];
|
||||
};
|
||||
};
|
||||
};
|
||||
chatRoom: {
|
||||
params: {
|
||||
roomId: string;
|
||||
};
|
||||
events: {
|
||||
message: (payload: ChatMessageLite) => void;
|
||||
deleted: (payload: ChatMessageLite['id']) => void;
|
||||
react: (payload: {
|
||||
reaction: string;
|
||||
user?: UserLite;
|
||||
messageId: ChatMessageLite['id'];
|
||||
}) => void;
|
||||
unreact: (payload: {
|
||||
reaction: string;
|
||||
user?: UserLite;
|
||||
messageId: ChatMessageLite['id'];
|
||||
}) => void;
|
||||
};
|
||||
receives: {
|
||||
read: {
|
||||
id: ChatMessageLite['id'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type NoteUpdatedEvent = { id: Note['id'] } & ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue