Merge branch 'feat/note-edit-history' into 'develop'

feat: Add post edit history

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10714
This commit is contained in:
naskya 2024-04-01 17:44:18 +00:00
commit 82dff9beb1
24 changed files with 409 additions and 21 deletions

View file

@ -2,6 +2,10 @@
Breaking changes are indicated by the :warning: icon. Breaking changes are indicated by the :warning: icon.
## Unreleased
- Added `notes/history` endpoint.
## v20240319 ## v20240319
- :warning: `followingCount` and `followersCount` in `users/show` will be `null` (instead of 0) if these values are unavailable. - :warning: `followingCount` and `followersCount` in `users/show` will be `null` (instead of 0) if these values are unavailable.

View file

@ -1,6 +1,7 @@
BEGIN; BEGIN;
DELETE FROM "migrations" WHERE name IN ( DELETE FROM "migrations" WHERE name IN (
'ExpandNoteEdit1711936358554',
'markLocalFilesNsfwByDefault1709305200000', 'markLocalFilesNsfwByDefault1709305200000',
'FixMutingIndices1710690239308', 'FixMutingIndices1710690239308',
'NoteFile1710304584214', 'NoteFile1710304584214',
@ -19,6 +20,9 @@ DELETE FROM "migrations" WHERE name IN (
'RemoveNativeUtilsMigration1705877093218' 'RemoveNativeUtilsMigration1705877093218'
); );
-- expand-note-edit
ALTER TABLE "note_edit" DROP COLUMN "emojis";
-- markLocalFilesNsfwByDefault -- markLocalFilesNsfwByDefault
ALTER TABLE "meta" DROP COLUMN "markLocalFilesNsfwByDefault"; ALTER TABLE "meta" DROP COLUMN "markLocalFilesNsfwByDefault";

View file

@ -2234,3 +2234,4 @@ autocorrectNoteLanguage: "Show a warning if the post language does not match the
result" result"
incorrectLanguageWarning: "It looks like your post is in {detected}, but you selected incorrectLanguageWarning: "It looks like your post is in {detected}, but you selected
{current}.\nWould you like to set the language to {detected} instead?" {current}.\nWould you like to set the language to {detected} instead?"
noteEditHistory: "Post edit history"

View file

@ -2060,3 +2060,4 @@ noAltTextWarning: 有些附件没有描述。您是否忘记写描述了?
showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告 showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告
autocorrectNoteLanguage: 当帖子语言不符合自动检测的结果的时候显示警告 autocorrectNoteLanguage: 当帖子语言不符合自动检测的结果的时候显示警告
incorrectLanguageWarning: "看上去您帖子使用的语言是{detected},但您选择的语言是{current}。\n要改为以{detected}发帖吗?" incorrectLanguageWarning: "看上去您帖子使用的语言是{detected},但您选择的语言是{current}。\n要改为以{detected}发帖吗?"
noteEditHistory: "帖子编辑历史"

View file

@ -16,6 +16,7 @@ pub struct Model {
pub file_ids: Vec<String>, pub file_ids: Vec<String>,
#[sea_orm(column_name = "updatedAt")] #[sea_orm(column_name = "updatedAt")]
pub updated_at: DateTimeWithTimeZone, pub updated_at: DateTimeWithTimeZone,
pub emojis: Vec<String>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -0,0 +1,15 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class ExpandNoteEdit1711936358554 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "note_edit" ADD "emojis" character varying(128) array NOT NULL DEFAULT '{}'::varchar[]
`);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "note_edit" DROP COLUMN "emojis"
`);
}
}

View file

@ -8,6 +8,7 @@ import { decodeReaction } from "./reaction-lib.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import { query } from "@/prelude/url.js"; import { query } from "@/prelude/url.js";
import { redisClient } from "@/db/redis.js"; import { redisClient } from "@/db/redis.js";
import type { NoteEdit } from "@/models/entities/note-edit.js";
const cache = new Cache<Emoji | null>("populateEmojis", 60 * 60 * 12); const cache = new Cache<Emoji | null>("populateEmojis", 60 * 60 * 12);
@ -110,6 +111,23 @@ export async function populateEmojis(
return emojis.filter((x): x is PopulatedEmoji => x != null); return emojis.filter((x): x is PopulatedEmoji => x != null);
} }
export function aggregateNoteEditEmojis(
noteEdits: NoteEdit[],
sourceHost: string | null,
) {
let emojis: string[] = [];
for (const noteEdit of noteEdits) {
emojis = emojis.concat(noteEdit.emojis);
}
emojis = Array.from(new Set(emojis));
return emojis
.map((e) => parseEmojiStr(e, sourceHost))
.filter((x) => x.name != null) as {
name: string;
host: string | null;
}[];
}
export function aggregateNoteEmojis(notes: Note[]) { export function aggregateNoteEmojis(notes: Note[]) {
let emojis: { name: string | null; host: string | null }[] = []; let emojis: { name: string | null; host: string | null }[] = [];
for (const note of notes) { for (const note of notes) {
@ -145,7 +163,7 @@ export function aggregateNoteEmojis(notes: Note[]) {
} }
/** /**
* * Get the given list of emojis from the database and adds them to the cache
*/ */
export async function prefetchEmojis( export async function prefetchEmojis(
emojis: { name: string; host: string | null }[], emojis: { name: string; host: string | null }[],

View file

@ -50,4 +50,11 @@ export class NoteEdit {
comment: "The updated date of the Note.", comment: "The updated date of the Note.",
}) })
public updatedAt: Date; public updatedAt: Date;
@Column("varchar", {
length: 128,
array: true,
default: "{}",
})
public emojis: string[];
} }

View file

@ -65,14 +65,14 @@ import { UserPending } from "./entities/user-pending.js";
import { InstanceRepository } from "./repositories/instance.js"; import { InstanceRepository } from "./repositories/instance.js";
import { Webhook } from "./entities/webhook.js"; import { Webhook } from "./entities/webhook.js";
import { UserIp } from "./entities/user-ip.js"; import { UserIp } from "./entities/user-ip.js";
import { NoteEdit } from "./entities/note-edit.js";
import { NoteFileRepository } from "./repositories/note-file.js"; import { NoteFileRepository } from "./repositories/note-file.js";
import { NoteEditRepository } from "./repositories/note-edit.js";
export const Announcements = db.getRepository(Announcement); export const Announcements = db.getRepository(Announcement);
export const AnnouncementReads = db.getRepository(AnnouncementRead); export const AnnouncementReads = db.getRepository(AnnouncementRead);
export const Apps = AppRepository; export const Apps = AppRepository;
export const Notes = NoteRepository; export const Notes = NoteRepository;
export const NoteEdits = db.getRepository(NoteEdit); export const NoteEdits = NoteEditRepository;
export const NoteFiles = NoteFileRepository; export const NoteFiles = NoteFileRepository;
export const NoteFavorites = NoteFavoriteRepository; export const NoteFavorites = NoteFavoriteRepository;
export const NoteWatchings = db.getRepository(NoteWatching); export const NoteWatchings = db.getRepository(NoteWatching);

View file

@ -0,0 +1,44 @@
import { db } from "@/db/postgre.js";
import { NoteEdit } from "@/models/entities/note-edit.js";
import type { Note } from "@/models/entities/note.js";
import { awaitAll } from "@/prelude/await-all.js";
import type { Packed } from "@/misc/schema.js";
import { DriveFiles } from "../index.js";
import {
aggregateNoteEditEmojis,
populateEmojis,
prefetchEmojis,
} from "@/misc/populate-emojis.js";
export const NoteEditRepository = db.getRepository(NoteEdit).extend({
async pack(noteEdit: NoteEdit, sourceNote: Note) {
const packed: Packed<"NoteEdit"> = await awaitAll({
id: noteEdit.id,
noteId: noteEdit.noteId,
updatedAt: noteEdit.updatedAt.toISOString(),
text: noteEdit.text,
cw: noteEdit.cw,
fileIds: noteEdit.fileIds,
files: DriveFiles.packMany(noteEdit.fileIds),
emojis: populateEmojis(noteEdit.emojis, sourceNote.userHost),
});
return packed;
},
async packMany(noteEdits: NoteEdit[], sourceNote: Note) {
if (noteEdits.length === 0) return [];
await prefetchEmojis(
aggregateNoteEditEmojis(noteEdits, sourceNote.userHost),
);
const promises = await Promise.allSettled(
noteEdits.map((n) => this.pack(n, sourceNote)),
);
// filter out rejected promises, only keep fulfilled values
return promises.flatMap((result) =>
result.status === "fulfilled" ? [result.value] : [],
);
},
});

View file

@ -16,7 +16,7 @@ export const packedNoteEdit = {
}, },
note: { note: {
type: "object", type: "object",
optional: false, optional: true,
nullable: false, nullable: false,
ref: "Note", ref: "Note",
}, },
@ -39,11 +39,27 @@ export const packedNoteEdit = {
fileIds: { fileIds: {
type: "array", type: "array",
optional: true, optional: true,
nullable: true, nullable: false,
items: { items: {
type: "string", type: "string",
format: "id", format: "id",
}, },
}, },
files: {
type: "array",
optional: true,
nullable: false,
items: {
type: "object",
optional: false,
nullable: false,
ref: "DriveFile",
},
},
emojis: {
type: "object",
optional: true,
nullable: true,
},
}, },
} as const; } as const;

View file

@ -773,6 +773,7 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
cw: note.cw, cw: note.cw,
fileIds: note.fileIds, fileIds: note.fileIds,
updatedAt: update.updatedAt, updatedAt: update.updatedAt,
emojis: note.emojis,
}); });
publishing = true; publishing = true;

View file

@ -240,6 +240,7 @@ import * as ep___notes_conversation from "./endpoints/notes/conversation.js";
import * as ep___notes_create from "./endpoints/notes/create.js"; import * as ep___notes_create from "./endpoints/notes/create.js";
import * as ep___notes_delete from "./endpoints/notes/delete.js"; import * as ep___notes_delete from "./endpoints/notes/delete.js";
import * as ep___notes_edit from "./endpoints/notes/edit.js"; import * as ep___notes_edit from "./endpoints/notes/edit.js";
import * as ep___notes_history from "./endpoints/notes/history.js";
import * as ep___notes_favorites_create from "./endpoints/notes/favorites/create.js"; import * as ep___notes_favorites_create from "./endpoints/notes/favorites/create.js";
import * as ep___notes_favorites_delete from "./endpoints/notes/favorites/delete.js"; import * as ep___notes_favorites_delete from "./endpoints/notes/favorites/delete.js";
import * as ep___notes_featured from "./endpoints/notes/featured.js"; import * as ep___notes_featured from "./endpoints/notes/featured.js";
@ -583,6 +584,7 @@ const eps = [
["notes/create", ep___notes_create], ["notes/create", ep___notes_create],
["notes/delete", ep___notes_delete], ["notes/delete", ep___notes_delete],
["notes/edit", ep___notes_edit], ["notes/edit", ep___notes_edit],
["notes/history", ep___notes_history],
["notes/favorites/create", ep___notes_favorites_create], ["notes/favorites/create", ep___notes_favorites_create],
["notes/favorites/delete", ep___notes_favorites_delete], ["notes/favorites/delete", ep___notes_favorites_delete],
["notes/featured", ep___notes_featured], ["notes/featured", ep___notes_featured],

View file

@ -621,6 +621,7 @@ export default define(meta, paramDef, async (ps, user) => {
cw: note.cw, cw: note.cw,
fileIds: note.fileIds, fileIds: note.fileIds,
updatedAt: new Date(), updatedAt: new Date(),
emojis: note.emojis,
}); });
publishing = true; publishing = true;
@ -639,7 +640,7 @@ export default define(meta, paramDef, async (ps, user) => {
(async () => { (async () => {
const noteActivity = await renderNote(note, false); const noteActivity = await renderNote(note, false);
noteActivity.updated = note.updatedAt.toISOString(); noteActivity.updated = new Date().toISOString();
const updateActivity = renderUpdate(noteActivity, user); const updateActivity = renderUpdate(noteActivity, user);
updateActivity.to = noteActivity.to; updateActivity.to = noteActivity.to;
updateActivity.cc = noteActivity.cc; updateActivity.cc = noteActivity.cc;

View file

@ -0,0 +1,67 @@
import { NoteEdits } from "@/models/index.js";
import define from "@/server/api/define.js";
import { ApiError } from "@/server/api/error.js";
import { getNote } from "@/server/api/common/getters.js";
import type { NoteEdit } from "@/models/entities/note-edit.js";
export const meta = {
tags: ["notes"],
requireCredential: false,
requireCredentialPrivateMode: true,
description: "Get edit history of a note",
res: {
type: "array",
optional: false,
nullable: true,
items: {
type: "object",
optional: false,
nullable: false,
ref: "NoteEdit",
},
},
errors: {
noSuchNote: {
message: "No such note.",
code: "NO_SUCH_NOTE",
id: "e1035875-9551-45ec-afa8-1ded1fcb53c8",
},
},
} as const;
export const paramDef = {
type: "object",
properties: {
noteId: {
type: "string",
format: "misskey:id",
},
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
offset: { type: "integer", default: 0 },
},
required: ["noteId"],
} as const;
export default define(meta, paramDef, async (ps, user) => {
const note = await getNote(ps.noteId, user).catch((err) => {
if (err.id === "9725d0ce-ba28-4dde-95a7-2cbb2c15de24")
throw new ApiError(meta.errors.noSuchNote);
throw err;
});
const history: NoteEdit[] = await NoteEdits.find({
where: {
noteId: note.id,
},
take: ps.limit,
skip: ps.offset,
order: {
id: "DESC",
},
});
return await NoteEdits.packMany(history, note);
});

View file

@ -10,7 +10,7 @@ export default defineComponent({
props: { props: {
items: { items: {
type: Array as PropType< type: Array as PropType<
{ id: string; createdAt: string; _shouldInsertAd_: boolean }[] { id: string; createdAt: string; _shouldInsertAd_?: boolean }[]
>, >,
required: true, required: true,
}, },

View file

@ -2,7 +2,7 @@
<div <div
v-if="!muted.muted" v-if="!muted.muted"
v-show="!isDeleted" v-show="!isDeleted"
:id="appearNote.id" :id="appearNote.historyId || appearNote.id"
ref="el" ref="el"
v-hotkey="keymap" v-hotkey="keymap"
v-size="{ max: [500, 350] }" v-size="{ max: [500, 350] }"
@ -91,6 +91,9 @@
:style="{ :style="{
cursor: expandOnNoteClick && !detailedView ? 'pointer' : '', cursor: expandOnNoteClick && !detailedView ? 'pointer' : '',
}" }"
:class="{
history: appearNote.historyId,
}"
@contextmenu.stop="onContextmenu" @contextmenu.stop="onContextmenu"
@click="noteClick" @click="noteClick"
> >
@ -154,7 +157,12 @@
{{ appearNote.channel.name }}</MkA {{ appearNote.channel.name }}</MkA
> >
</div> </div>
<footer ref="footerEl" class="footer" tabindex="-1"> <footer
v-show="!hideFooter"
ref="footerEl"
class="footer"
tabindex="-1"
>
<XReactionsViewer <XReactionsViewer
v-if="enableEmojiReactions" v-if="enableEmojiReactions"
ref="reactionsViewer" ref="reactionsViewer"
@ -312,6 +320,7 @@ const props = defineProps<{
pinned?: boolean; pinned?: boolean;
detailedView?: boolean; detailedView?: boolean;
collapsedReply?: boolean; collapsedReply?: boolean;
hideFooter?: boolean;
}>(); }>();
const inChannel = inject("inChannel", null); const inChannel = inject("inChannel", null);
@ -420,11 +429,13 @@ const keymap = {
s: () => showContent.value !== showContent.value, s: () => showContent.value !== showContent.value,
}; };
useNoteCapture({ if (appearNote.value.historyId == null) {
rootEl: el, useNoteCapture({
note: appearNote, rootEl: el,
isDeletedRef: isDeleted, note: appearNote,
}); isDeletedRef: isDeleted,
});
}
function reply(viaKeyboard = false): void { function reply(viaKeyboard = false): void {
pleaseLogin(); pleaseLogin();
@ -851,6 +862,9 @@ defineExpose({
overflow: clip; overflow: clip;
padding: 20px 32px 10px; padding: 20px 32px 10px;
margin-top: -16px; margin-top: -16px;
&.history {
margin-top: -90px !important;
}
&:first-child, &:first-child,
&:nth-child(2) { &:nth-child(2) {

View file

@ -118,10 +118,9 @@ const emit = defineEmits<{
(ev: "status", error: boolean): void; (ev: "status", error: boolean): void;
}>(); }>();
interface Item { type Item = Endpoints[typeof props.pagination.endpoint]["res"] & {
id: string; id: string;
[another: string]: unknown; };
}
const rootEl = ref<HTMLElement>(); const rootEl = ref<HTMLElement>();
const items = ref<Item[]>([]); const items = ref<Item[]>([]);

View file

@ -1,5 +1,16 @@
import type { entities } from "firefish-js"; import type { entities } from "firefish-js";
export const notePage = (note: entities.Note) => { export function notePage(
note: entities.Note,
options?: {
historyPage?: boolean;
},
) {
if (options?.historyPage) {
return `/notes/${note.id}/history`;
}
if (note.historyId) {
return `/notes/${note.id}/history#${note.historyId}`;
}
return `/notes/${note.id}`; return `/notes/${note.id}`;
}; }

View file

@ -0,0 +1,135 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader :display-back-button="true"
/></template>
<MkSpacer :content-max="800">
<MkLoading v-if="!loaded" />
<MkPagination
v-else
ref="pagingComponent"
v-slot="{ items }: { items: entities.NoteEdit[] }"
:pagination="pagination"
>
<div ref="tlEl" class="giivymft noGap">
<XList
v-slot="{ item }: { item: entities.Note }"
:items="convertNoteEditsToNotes(items)"
class="notes"
:no-gap="true"
>
<XNote
:key="item.id"
class="qtqtichx"
:note="item"
:hide-footer="true"
:detailed-view="true"
/>
</XList>
</div>
</MkPagination>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from "vue";
import MkPagination from "@/components/MkPagination.vue";
import type { Paging } from "@/components/MkPagination.vue";
import { api } from "@/os";
import XList from "@/components/MkDateSeparatedList.vue";
import XNote from "@/components/MkNote.vue";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import icon from "@/scripts/icon";
import type { entities } from "firefish-js";
const pagingComponent = ref<InstanceType<typeof MkPagination>>();
const props = defineProps<{
noteId: string;
}>();
const pagination: Paging = {
endpoint: "notes/history" as const,
limit: 10,
offsetMode: true,
params: computed(() => ({
noteId: props.noteId,
})),
};
definePageMetadata(
computed(() => ({
title: i18n.ts.noteEditHistory,
icon: `${icon("ph-clock-countdown")}`,
})),
);
const note = ref<entities.Note>({} as entities.Note);
const loaded = ref(false);
onMounted(() => {
api("notes/show", {
noteId: props.noteId,
}).then((res) => {
// Remove unnecessary parts
res.renote = undefined;
res.renoteId = null;
res.reply = undefined;
res.replyId = null;
note.value = res;
loaded.value = true;
});
});
function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
const now: entities.NoteEdit = {
id: "EditionNow",
noteId: note.value.id,
updatedAt: note.value.createdAt,
text: note.value.text,
cw: note.value.cw,
files: note.value.files,
fileIds: note.value.fileIds,
emojis: note.value.emojis,
};
return [now]
.concat(noteEdits)
.map((noteEdit: entities.NoteEdit, index, arr): entities.Note => {
return Object.assign({}, note.value, {
historyId: noteEdit.id,
// Conversion from updatedAt to createdAt
// The createdAt of a edition's content is actually the updatedAt of the previous one.
createdAt: arr[(index + 1) % arr.length].updatedAt,
text: noteEdit.text,
cw: noteEdit.cw,
_shouldInsertAd_: false,
files: noteEdit.files,
fileIds: noteEdit.fileIds,
emojis: note.value.emojis.concat(noteEdit.emojis),
});
});
}
</script>
<style lang="scss" scoped>
.giivymft {
&.noGap {
> .notes {
background: var(--panel) !important;
border-radius: var(--radius);
}
}
&:not(.noGap) {
> .notes {
.qtqtichx {
background: var(--panel);
border-radius: var(--radius);
}
}
}
}
</style>

View file

@ -39,6 +39,11 @@ export const routes = [
path: "/notes/:noteId", path: "/notes/:noteId",
component: page(() => import("./pages/note.vue")), component: page(() => import("./pages/note.vue")),
}, },
{
name: "note-history",
path: "/notes/:noteId/history",
component: page(() => import("./pages/note-history.vue")),
},
{ {
path: "/clips/:clipId", path: "/clips/:clipId",
component: page(() => import("./pages/clip.vue")), component: page(() => import("./pages/clip.vue")),

View file

@ -11,6 +11,10 @@ import { noteActions } from "@/store";
import { shareAvailable } from "@/scripts/share-available"; import { shareAvailable } from "@/scripts/share-available";
import { getUserMenu } from "@/scripts/get-user-menu"; import { getUserMenu } from "@/scripts/get-user-menu";
import icon from "@/scripts/icon"; import icon from "@/scripts/icon";
import { useRouter } from "@/router";
import { notePage } from "@/filters/note";
const router = useRouter();
export function getNoteMenu(props: { export function getNoteMenu(props: {
note: entities.Note; note: entities.Note;
@ -73,6 +77,10 @@ export function getNoteMenu(props: {
}); });
} }
function showEditHistory(): void {
router.push(notePage(appearNote, { historyPage: true }));
}
function makePrivate(): void { function makePrivate(): void {
os.confirm({ os.confirm({
type: "warning", type: "warning",
@ -288,6 +296,8 @@ export function getNoteMenu(props: {
noteId: appearNote.id, noteId: appearNote.id,
}); });
const isEdited = !!appearNote.updatedAt;
const isAppearAuthor = appearNote.userId === me.id; const isAppearAuthor = appearNote.userId === me.id;
menu = [ menu = [
@ -361,6 +371,13 @@ export function getNoteMenu(props: {
action: () => togglePin(true), action: () => togglePin(true),
} }
: undefined, : undefined,
isEdited
? {
icon: `${icon("ph-clock-countdown")}`,
text: i18n.ts.noteEditHistory,
action: () => showEditHistory(),
}
: undefined,
instance.translatorAvailable instance.translatorAvailable
? { ? {
icon: `${icon("ph-translate")}`, icon: `${icon("ph-translate")}`,

View file

@ -22,6 +22,7 @@ import type {
MeDetailed, MeDetailed,
MessagingMessage, MessagingMessage,
Note, Note,
NoteEdit,
NoteFavorite, NoteFavorite,
NoteReaction, NoteReaction,
Notification, Notification,
@ -657,6 +658,14 @@ export type Endpoints = {
}; };
res: Note[]; res: Note[];
}; };
"notes/history": {
req: {
noteId: Note["id"];
limit?: number;
offset?: number;
};
res: NoteEdit[];
};
"notes/recommended-timeline": { "notes/recommended-timeline": {
req: { req: {
limit?: number; limit?: number;

View file

@ -143,9 +143,9 @@ export type Note = {
user: User; user: User;
userId: User["id"]; userId: User["id"];
reply?: Note; reply?: Note;
replyId: Note["id"]; replyId: Note["id"] | null;
renote?: Note; renote?: Note;
renoteId: Note["id"]; renoteId: Note["id"] | null;
files: DriveFile[]; files: DriveFile[];
fileIds: DriveFile["id"][]; fileIds: DriveFile["id"][];
visibility: "public" | "home" | "followers" | "specified"; visibility: "public" | "home" | "followers" | "specified";
@ -174,6 +174,22 @@ export type Note = {
url?: string; url?: string;
updatedAt?: DateString; updatedAt?: DateString;
isHidden?: boolean; isHidden?: boolean;
/** if the note is a history */
historyId?: ID;
};
export type NoteEdit = {
id: string;
noteId: string;
text: string | null;
cw: string | null;
updatedAt: string;
fileIds: DriveFile["id"][];
files: DriveFile[];
emojis: {
name: string;
url: string;
}[];
}; };
export type NoteReaction = { export type NoteReaction = {