diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 292174629..0a21f2032 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -112,6 +112,7 @@ redis: # apiKey: '' # ssl: true # index: '' +# scope: global # ┌───────────────┐ #───┘ ID generation └─────────────────────────────────────────── @@ -144,15 +145,22 @@ id: 'aidx' # Job concurrency per worker # deliverJobConcurrency: 128 # inboxJobConcurrency: 16 +# relashionshipJobConcurrency: 16 +# What's relashionshipJob?: +# Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations. # Job rate limiter # deliverJobPerSec: 128 # inboxJobPerSec: 16 +# relashionshipJobPerSec: 64 # Job attempts # deliverJobMaxAttempts: 12 # inboxJobMaxAttempts: 8 +# Local address used for outgoing requests +#outgoingAddress: 127.0.0.1 + # IP address family used for outgoing request (ipv4, ipv6 or dual) #outgoingAddressFamily: ipv4 @@ -175,8 +183,15 @@ proxyBypassHosts: #mediaProxy: https://example.com/proxy # Proxy remote files (default: true) +# Proxy remote files by this instance or mediaProxy to prevent remote files from running in remote domains. proxyRemoteFiles: true +# Movie Thumbnail Generation URL +# There is no reference implementation. +# For example, Misskey will point to the following URL: +# https://example.com/thumbnail.webp?thumbnail=1&url=https%3A%2F%2Fstorage.example.com%2Fpath%2Fto%2Fvideo.mp4 +#videoThumbnailGenerator: https://example.com + # Sign to ActivityPub GET request (default: true) signToActivityPubGet: true diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index ada3db2d5..fb6c9ebdf 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -8,7 +8,7 @@ jobs: run: runs-on: ubuntu-latest steps: - - uses: actions/first-interaction@v1.2.0 + - uses: actions/first-interaction@v1.3.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} issue-message: | diff --git a/locales/en-US.yml b/locales/en-US.yml index 7f30959c1..7f5239ae8 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -55,6 +55,7 @@ addToAntenna: "Add to antenna" sendMessage: "Send a message" copyRSS: "Copy RSS" copyUsername: "Copy username" +openRemoteProfile: "Open remote profile" copyUserId: "Copy user ID" copyNoteId: "Copy note ID" copyFileId: "Copy file ID" @@ -110,7 +111,6 @@ renote: "Boost" unrenote: "Remove boost" renoted: "Boosted." quoted: "Quoted." -rmquote: "Removed quote." rmboost: "Unboosted." cantRenote: "This post can't be boosted." cantReRenote: "A boost can't be boosted." @@ -891,6 +891,7 @@ continueThread: "View thread continuation" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" incorrectPassword: "Incorrect password." voteConfirm: "Confirm your vote for \"{choice}\"?" +voteConfirmMulti: "Confirm your vote for \"{choice}\"?\n You can choose more options after confirmation." hide: "Hide" useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile" welcomeBackWithName: "Welcome back, {name}" @@ -987,6 +988,7 @@ cannotLoad: "Unable to load" numberOfProfileView: "Profile views" like: "Like" unlike: "Unlike" +defaultLike: "Default like emoji" numberOfLikes: "Likes" show: "Show" neverShow: "Don't show again" @@ -1855,6 +1857,14 @@ _ago: monthsAgo: "{n}mo ago" yearsAgo: "{n}y ago" invalid: "None" +_timeIn: + seconds: "in {n} seconds" + minutes: "in {n} minutes" + hours: "in {n} hours" + days: "in {n} days" + weeks: "in {n} weeks" + months: "in {n} months" + years: "in {n} years" _time: second: "Second(s)" minute: "Minute(s)" @@ -1980,6 +1990,7 @@ _widgets: _userList: chooseList: "Select a list" clicker: "Clicker" + search: "Search" _cw: hide: "Hide" show: "Show content" @@ -2007,6 +2018,7 @@ _poll: remainingHours: "{h} hour(s) {m} minute(s) remaining" remainingMinutes: "{m} minute(s) {s} second(s) remaining" remainingSeconds: "{s} second(s) remaining" + multiple: "Multiple choices" _visibility: public: "Public" publicDescription: "Your note will be visible for all users" diff --git a/locales/index.d.ts b/locales/index.d.ts index 8356046a6..9aeaeb9e5 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -58,6 +58,7 @@ export interface Locale { "sendMessage": string; "copyRSS": string; "copyUsername": string; + "openRemoteProfile": string; "copyUserId": string; "copyNoteId": string; "copyFileId": string; @@ -114,7 +115,6 @@ export interface Locale { "renoted": string; "quoted": string; "rmboost": string; - "rmquote": string; "cantRenote": string; "cantReRenote": string; "quote": string; @@ -894,6 +894,7 @@ export interface Locale { "deleteAccountConfirm": string; "incorrectPassword": string; "voteConfirm": string; + "voteConfirmMulti": string; "hide": string; "useDrawerReactionPickerForMobile": string; "welcomeBackWithName": string; @@ -990,6 +991,7 @@ export interface Locale { "numberOfProfileView": string; "like": string; "unlike": string; + "defaultLike": string; "numberOfLikes": string; "show": string; "neverShow": string; @@ -2125,6 +2127,7 @@ export interface Locale { "chooseList": string; }; "clicker": string; + "search": string; }; "_cw": { "hide": string; @@ -2154,6 +2157,7 @@ export interface Locale { "remainingHours": string; "remainingMinutes": string; "remainingSeconds": string; + "multiple": string; }; "_visibility": { "public": string; diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 570629e82..fcf1a099e 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -54,6 +54,7 @@ addToAntenna: "Aggiungi all'antenna" sendMessage: "Invia messaggio" copyRSS: "Copia RSS" copyUsername: "Copia nome utente" +openRemoteProfile: "Apri profilo remoto" copyUserId: "Copia ID del profilo" copyNoteId: "Copia ID della Nota" copyFileId: "Copia ID del file" @@ -731,6 +732,8 @@ thisIsExperimentalFeature: "Questa è una funzionalità sperimentale. Potrebbe e developer: "Sviluppatore" makeExplorable: "Profilo visibile pubblicamente nella pagina \"Esplora\"" makeExplorableDescription: "Disabilitando questa opzione, il tuo profilo non verrà elencato nella pagina \"Esplora\"." +makeIndexable: "Non indicizzare le note pubbliche" +makeIndexableDescription: "Le tue note pubbliche non saranno cercabili" showGapBetweenNotesInTimeline: "Mostrare un intervallo tra le note sulla timeline" duplicate: "Duplica" left: "Sinistra" @@ -964,6 +967,7 @@ cannotLoad: "Caricamento impossibile" numberOfProfileView: "Visualizzazioni profilo" like: "Mi piace!" unlike: "Non mi piace" +defaultLike: "Emoji predefinita per \"mi piace\"" numberOfLikes: "Numero di Like" show: "Visualizza" neverShow: "Non mostrare più" @@ -1266,6 +1270,8 @@ _serverSettings: shortName: "Abbreviazione" shortNameDescription: "Un'abbreviazione o un nome comune che può essere visualizzato al posto del nome ufficiale lungo del server." fanoutTimelineDescription: "Attivando questa funzionalità migliori notevolmente la capacità delle Timeline di collezionare Note, riducendo il carico sul database. Tuttavia, aumenterà l'impiego di memoria RAM per Redis. Disattiva se il tuo server ha poca RAM o la funzionalità è irregolare." + fanoutTimelineDbFallback: "Ripiega sul database" + fanoutTimelineDbFallbackDescription: "Attivando questa funzionalità, nel caso che il contenuto di una Timeline non sia presente nella cache, verrà consultato il database. Disattivandola, il carico sul database sarà ulteriormente ridotto, ma le Timeline saranno limitate" _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" @@ -1702,6 +1708,7 @@ _serverDisconnectedBehavior: reload: "Ricarica automaticamente" dialog: "Apri avviso in finestra" quiet: "Visualizza avviso in modo discreto" + disabled: "Non visualizzare l'avviso" _channel: create: "Nuovo canale" edit: "Gerisci canale" @@ -1817,6 +1824,14 @@ _ago: monthsAgo: "{n} mesi fa" yearsAgo: "{n} anni fa" invalid: "Niente da visualizzare" +_timeIn: + seconds: "fra {n} secondi" + minutes: "fra {n} minuti" + hours: "fra {n} ore" + days: "fra {n} giorni" + weeks: "fra {n} settimane" + months: "fra {n} mesi" + years: "fra {n} anni" _time: second: "s" minute: "min" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 25ee9b188..2f81a25cb 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -55,6 +55,7 @@ addToAntenna: "アンテナに追加" sendMessage: "メッセージを送信" copyRSS: "RSSをコピー" copyUsername: "ユーザー名をコピー" +openRemoteProfile: "リモートプロファイルを開く" copyUserId: "ユーザーIDをコピー" copyNoteId: "ノートIDをコピー" copyFileId: "ファイルIDをコピー" @@ -111,7 +112,6 @@ unrenote: "リノート解除" renoted: "ブースト。" quoted: "引用。" rmboost: "アンブースト。" -rmquote: "引用を削除しました。" cantRenote: "この投稿はリノートできません。" cantReRenote: "リノートをリノートすることはできません。" quote: "引用" @@ -891,6 +891,7 @@ continueThread: "さらにスレッドを見る" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" incorrectPassword: "パスワードが間違っています。" voteConfirm: "「{choice}」に投票しますか?" +voteConfirmMulti: "「{choice}」に投票しますか?\n 確認後、選択肢を増やすことができます。" hide: "隠す" useDrawerReactionPickerForMobile: "モバイルデバイスのときドロワーで表示" welcomeBackWithName: "おかえりなさい、{name}さん" @@ -987,6 +988,7 @@ cannotLoad: "読み込めません" numberOfProfileView: "プロフィール表示回数" like: "いいね!" unlike: "いいねを解除" +defaultLike: "絵文字のようなデフォルト" numberOfLikes: "いいね数" show: "表示" neverShow: "今後表示しない" @@ -2029,6 +2031,7 @@ _widgets: _userList: chooseList: "リストを選択" clicker: "クリッカー" + search: "検索" _cw: hide: "隠す" @@ -2058,6 +2061,7 @@ _poll: remainingHours: "終了まであと{h}時間{m}分" remainingMinutes: "終了まであと{m}分{s}秒" remainingSeconds: "終了まであと{s}秒" + multiple: "複数の選択肢" _visibility: public: "パブリック" @@ -2403,7 +2407,7 @@ _externalResourceInstaller: _themeInstallFailed: title: "テーマのインストールに失敗しました" description: "テーマのインストール中に問題が発生しました。もう一度お試しください。エラーの詳細はJavascriptコンソールをご覧ください。" - + _animatedMFM: play: "MFMアニメーションを再生" stop: "MFMアニメーション停止" diff --git a/package.json b/package.json index 6e8bb9051..071812e47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharkey", - "version": "2023.11.1", + "version": "2023.11.2", "codename": "shonk", "repository": { "type": "git", diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 75335f6ff..5a1fe3d08 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -12,7 +12,7 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiEmoji } from '@/models/Emoji.js'; -import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js'; +import type { DriveFilesRepository, EmojisRepository, MiRole, MiUser } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -20,6 +20,7 @@ import { query } from '@/misc/prelude/url.js'; import type { Serialized } from '@/types.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import type { Config } from '@/config.js'; +import { DriveService } from './DriveService.js'; const parseEmojiStrRegexp = /^(\w+)(?:@([\w.-]+))?$/; @@ -38,11 +39,15 @@ export class CustomEmojiService implements OnApplicationShutdown { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + private utilityService: UtilityService, private idService: IdService, private emojiEntityService: EmojiEntityService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, + private driveService: DriveService, ) { this.cache = new MemoryKVCache(1000 * 60 * 60 * 12); @@ -259,6 +264,12 @@ export class CustomEmojiService implements OnApplicationShutdown { this.localEmojisCache.refresh(); + const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() }); + + if (file) { + await this.driveService.deleteFile(file, false, moderator ? moderator : undefined); + } + this.globalEventService.publishBroadcastStream('emojiDeleted', { emojis: [await this.emojiEntityService.packDetailed(emoji)], }); @@ -280,6 +291,12 @@ export class CustomEmojiService implements OnApplicationShutdown { for (const emoji of emojis) { await this.emojisRepository.delete(emoji.id); + const file = await this.driveFilesRepository.findOneBy({ url: emoji.originalUrl, userHost: emoji.host ? emoji.host : IsNull() }); + + if (file) { + await this.driveService.deleteFile(file, false, moderator ? moderator : undefined); + } + if (moderator) { this.moderationLogService.log(moderator, 'deleteCustomEmoji', { emojiId: emoji.id, diff --git a/packages/backend/src/core/NoteEditService.ts b/packages/backend/src/core/NoteEditService.ts index dfcff0e5b..e5918e6c2 100644 --- a/packages/backend/src/core/NoteEditService.ts +++ b/packages/backend/src/core/NoteEditService.ts @@ -14,18 +14,15 @@ import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mf import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { NoteEditRepository, ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository, PollsRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; import { IdService } from '@/core/IdService.js'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { MiPoll, type IPoll } from '@/models/Poll.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; import type { MiChannel } from '@/models/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MemorySingleCache } from '@/misc/cache.js'; -import type { MiUserProfile } from '@/models/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -35,7 +32,6 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { WebhookService } from '@/core/WebhookService.js'; -import { HashtagService } from '@/core/HashtagService.js'; import { QueueService } from '@/core/QueueService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -48,11 +44,7 @@ import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; -import { FeaturedService } from '@/core/FeaturedService.js'; import { FunoutTimelineService } from '@/core/FunoutTimelineService.js'; -import { AntennaService } from './AntennaService.js'; -import NotesChart from './chart/charts/notes.js'; -import PerUserNotesChart from './chart/charts/per-user-notes.js'; import { UtilityService } from '@/core/UtilityService.js'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; @@ -191,6 +183,9 @@ export class NoteEditService implements OnApplicationShutdown { @Inject(DI.noteEditRepository) private noteEditRepository: NoteEditRepository, + @Inject(DI.pollsRepository) + private pollsRepository: PollsRepository, + private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, private idService: IdService, @@ -201,18 +196,13 @@ export class NoteEditService implements OnApplicationShutdown { private notificationService: NotificationService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, - private hashtagService: HashtagService, - private antennaService: AntennaService, private webhookService: WebhookService, - private featuredService: FeaturedService, private remoteUserResolveService: RemoteUserResolveService, private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, private metaService: MetaService, private searchService: SearchService, - private notesChart: NotesChart, - private perUserNotesChart: PerUserNotesChart, private activeUsersChart: ActiveUsersChart, private instanceChart: InstanceChart, private utilityService: UtilityService, @@ -385,6 +375,10 @@ export class NoteEditService implements OnApplicationShutdown { update.hasPoll = !!data.poll; } + const poll = await this.pollsRepository.findOneBy({ noteId: oldnote.id }); + + const oldPoll = poll ? { choices: poll.choices, multiple: poll.multiple, expiresAt: poll.expiresAt } : null; + if (Object.keys(update).length > 0) { const exists = await this.noteEditRepository.findOneBy({ noteId: oldnote.id }); @@ -456,7 +450,7 @@ export class NoteEditService implements OnApplicationShutdown { })); } - if (data.poll != null) { + if (data.poll != null && JSON.stringify(data.poll) !== JSON.stringify(oldPoll)) { // Start transaction await this.db.transaction(async transactionalEntityManager => { await transactionalEntityManager.update(MiNote, oldnote.id, note); diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 0230c9a7b..2ee61eb54 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -278,14 +278,14 @@ export class QueueService { } @bindThis - public createImportMastoToDbJob(user: ThinUser, targets: string[]) { - const jobs = targets.map(rel => this.generateToDbJobData('importMastoToDb', { user, target: rel })); + public createImportMastoToDbJob(user: ThinUser, targets: string[], note: MiNote['id'] | null) { + const jobs = targets.map(rel => this.generateToDbJobData('importMastoToDb', { user, target: rel, note })); return this.dbQueue.addBulk(jobs); } @bindThis - public createImportPleroToDbJob(user: ThinUser, targets: string[]) { - const jobs = targets.map(rel => this.generateToDbJobData('importPleroToDb', { user, target: rel })); + public createImportPleroToDbJob(user: ThinUser, targets: string[], note: MiNote['id'] | null) { + const jobs = targets.map(rel => this.generateToDbJobData('importPleroToDb', { user, target: rel, note })); return this.dbQueue.addBulk(jobs); } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 4d8e075a3..7464593bb 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -171,6 +171,7 @@ export class ApRendererService { mediaType: file.webpublicType ?? file.type, url: this.driveFileEntityService.getPublicUrl(file), name: file.comment, + summary: file.comment, }; } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index b5ba8cd5a..ce3317ef7 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -11,7 +11,7 @@ export interface IObject { type: string | string[]; id?: string; name?: string | null; - summary?: string; + summary?: string | null; _misskey_summary?: string; published?: string; cc?: ApObject; diff --git a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts index 5c3578010..552b69d92 100644 --- a/packages/backend/src/queue/processors/ImportNotesProcessorService.ts +++ b/packages/backend/src/queue/processors/ImportNotesProcessorService.ts @@ -3,7 +3,7 @@ import * as vm from 'node:vm'; import { Inject, Injectable } from '@nestjs/common'; import { ZipReader } from 'slacc'; import { DI } from '@/di-symbols.js'; -import type { UsersRepository, DriveFilesRepository, MiDriveFile, MiNote, NotesRepository, MiUser } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository, MiDriveFile, MiNote, NotesRepository, MiUser, DriveFoldersRepository, MiDriveFolder } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DownloadService } from '@/core/DownloadService.js'; import { bindThis } from '@/decorators.js'; @@ -14,9 +14,10 @@ import { DriveService } from '@/core/DriveService.js'; import { MfmService } from '@/core/MfmService.js'; import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js'; import { extractApHashtagObjects } from '@/core/activitypub/models/tag.js'; +import { IdService } from '@/core/IdService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; -import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbKeyNoteImportToDbJobData } from '../types.js'; +import type { DbNoteImportToDbJobData, DbNoteImportJobData, DbNoteWithParentImportToDbJobData } from '../types.js'; @Injectable() export class ImportNotesProcessorService { @@ -29,6 +30,9 @@ export class ImportNotesProcessorService { @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, + @Inject(DI.driveFoldersRepository) + private driveFoldersRepository: DriveFoldersRepository, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -38,20 +42,21 @@ export class ImportNotesProcessorService { private apNoteService: ApNoteService, private driveService: DriveService, private downloadService: DownloadService, + private idService: IdService, private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('import-notes'); } @bindThis - private async uploadFiles(dir: string, user: MiUser) { + private async uploadFiles(dir: string, user: MiUser, folder?: MiDriveFolder['id']) { const fileList = fs.readdirSync(dir); for await (const file of fileList) { const name = `${dir}/${file}`; if (fs.statSync(name).isDirectory()) { - await this.uploadFiles(name, user); + await this.uploadFiles(name, user, folder); } else { - const exists = await this.driveFilesRepository.findOneBy({ name: file, userId: user.id }); + const exists = await this.driveFilesRepository.findOneBy({ name: file, userId: user.id, folderId: folder }); if (file.endsWith('.srt')) return; @@ -60,6 +65,7 @@ export class ImportNotesProcessorService { user: user, path: name, name: file, + folderId: folder, }); } } @@ -68,7 +74,7 @@ export class ImportNotesProcessorService { // Function was taken from Firefish and modified for our needs @bindThis - private async recreateChain(idField: string, replyField: string, arr: any[]): Promise { + private async recreateChain(idFieldPath: string[], replyFieldPath: string[], arr: any[], includeOrphans: boolean): Promise { type NotesMap = { [id: string]: any; }; @@ -77,28 +83,42 @@ export class ImportNotesProcessorService { const notesWaitingForParent: NotesMap = {}; for await (const note of arr) { - noteById[note[idField]] = note; + const noteId = idFieldPath.reduce( + (obj, step) => obj[step], + note, + ); + + noteById[noteId] = note; note.childNotes = []; - const children = notesWaitingForParent[note[idField]]; + const children = notesWaitingForParent[noteId]; if (children) { note.childNotes.push(...children); + delete notesWaitingForParent[noteId]; } - if (note[replyField] == null) { + const noteReplyId = replyFieldPath.reduce( + (obj, step) => obj[step], + note, + ); + if (noteReplyId == null) { notesTree.push(note); continue; } - const parent = noteById[note[replyField]]; + const parent = noteById[noteReplyId]; if (parent) { parent.childNotes.push(note); } else { - notesWaitingForParent[note[replyField]] ||= []; - notesWaitingForParent[note[replyField]].push(note); + notesWaitingForParent[noteReplyId] ||= []; + notesWaitingForParent[noteReplyId].push(note); } } + if (includeOrphans) { + notesTree.push(...Object.values(notesWaitingForParent).flat(1)); + } + return notesTree; } @@ -126,6 +146,12 @@ export class ImportNotesProcessorService { return; } + let folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id }); + if (folder == null) { + await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Imports', userId: job.data.user.id }); + folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id }); + } + const type = job.data.type; if (type === 'Twitter' || file.name.startsWith('twitter') && file.name.endsWith('.zip')) { @@ -164,7 +190,7 @@ export class ImportNotesProcessorService { const tweets = Object.keys(fakeWindow.window.YTD.tweets.part0).reduce((m, key, i, obj) => { return m.concat(fakeWindow.window.YTD.tweets.part0[key].tweet); }, []); - const processedTweets = await this.recreateChain("id_str", "in_reply_to_status_id_str", tweets); + const processedTweets = await this.recreateChain(['id_str'], ['in_reply_to_status_id_str'], tweets, false); this.queueService.createImportTweetsToDbJob(job.data.user, processedTweets, null); } finally { cleanup(); @@ -192,7 +218,12 @@ export class ImportNotesProcessorService { ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath)); const postsJson = fs.readFileSync(outputPath + '/your_activity_across_facebook/posts/your_posts__check_ins__photos_and_videos_1.json', 'utf-8'); const posts = JSON.parse(postsJson); - await this.uploadFiles(outputPath + '/your_activity_across_facebook/posts/media', user); + const facebookFolder = await this.driveFoldersRepository.findOneBy({ name: 'Facebook', userId: job.data.user.id, parentId: folder?.id }); + if (facebookFolder == null && folder) { + await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Facebook', userId: job.data.user.id, parentId: folder.id }); + const createdFolder = await this.driveFoldersRepository.findOneBy({ name: 'Facebook', userId: job.data.user.id, parentId: folder.id }); + if (createdFolder) await this.uploadFiles(outputPath + '/your_activity_across_facebook/posts/media', user, createdFolder.id); + } this.queueService.createImportFBToDbJob(job.data.user, posts); } finally { cleanup(); @@ -223,7 +254,12 @@ export class ImportNotesProcessorService { if (isInstagram) { const postsJson = fs.readFileSync(outputPath + '/content/posts_1.json', 'utf-8'); const posts = JSON.parse(postsJson); - await this.uploadFiles(outputPath + '/media/posts', user); + const igFolder = await this.driveFoldersRepository.findOneBy({ name: 'Instagram', userId: job.data.user.id, parentId: folder?.id }); + if (igFolder == null && folder) { + await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Instagram', userId: job.data.user.id, parentId: folder.id }); + const createdFolder = await this.driveFoldersRepository.findOneBy({ name: 'Instagram', userId: job.data.user.id, parentId: folder.id }); + if (createdFolder) await this.uploadFiles(outputPath + '/media/posts', user, createdFolder.id); + } this.queueService.createImportIGToDbJob(job.data.user, posts); } else if (isOutbox) { const actorJson = fs.readFileSync(outputPath + '/actor.json', 'utf-8'); @@ -232,12 +268,21 @@ export class ImportNotesProcessorService { if (isPleroma) { const outboxJson = fs.readFileSync(outputPath + '/outbox.json', 'utf-8'); const outbox = JSON.parse(outboxJson); - this.queueService.createImportPleroToDbJob(job.data.user, outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note')); + const processedToots = await this.recreateChain(['object', 'id'], ['object', 'inReplyTo'], outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'), true); + this.queueService.createImportPleroToDbJob(job.data.user, processedToots, null); } else { const outboxJson = fs.readFileSync(outputPath + '/outbox.json', 'utf-8'); const outbox = JSON.parse(outboxJson); - if (fs.existsSync(outputPath + '/media_attachments/files')) await this.uploadFiles(outputPath + '/media_attachments/files', user); - this.queueService.createImportMastoToDbJob(job.data.user, outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note')); + let mastoFolder = await this.driveFoldersRepository.findOneBy({ name: 'Mastodon', userId: job.data.user.id, parentId: folder?.id }); + if (mastoFolder == null && folder) { + await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Mastodon', userId: job.data.user.id, parentId: folder.id }); + mastoFolder = await this.driveFoldersRepository.findOneBy({ name: 'Mastodon', userId: job.data.user.id, parentId: folder.id }); + } + if (fs.existsSync(outputPath + '/media_attachments/files') && mastoFolder) { + await this.uploadFiles(outputPath + '/media_attachments/files', user, mastoFolder.id); + } + const processedToots = await this.recreateChain(['object', 'id'], ['object', 'inReplyTo'], outbox.orderedItems.filter((x: any) => x.type === 'Create' && x.object.type === 'Note'), true); + this.queueService.createImportMastoToDbJob(job.data.user, processedToots, null); } } } finally { @@ -260,7 +305,7 @@ export class ImportNotesProcessorService { const notesJson = fs.readFileSync(path, 'utf-8'); const notes = JSON.parse(notesJson); - const processedNotes = await this.recreateChain("id", "replyId", notes); + const processedNotes = await this.recreateChain(['id'], ['replyId'], notes, false); this.queueService.createImportKeyNotesToDbJob(job.data.user, processedNotes, null); cleanup(); } @@ -269,7 +314,7 @@ export class ImportNotesProcessorService { } @bindThis - public async processKeyNotesToDb(job: Bull.Job): Promise { + public async processKeyNotesToDb(job: Bull.Job): Promise { const note = job.data.target; const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { @@ -280,16 +325,25 @@ export class ImportNotesProcessorService { const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null; + const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id }); + if (folder == null) return; + const files: MiDriveFile[] = []; const date = new Date(note.createdAt); if (note.files && this.isIterable(note.files)) { + let keyFolder = await this.driveFoldersRepository.findOneBy({ name: 'Misskey', userId: job.data.user.id, parentId: folder.id }); + if (keyFolder == null) { + await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Misskey', userId: job.data.user.id, parentId: folder.id }); + keyFolder = await this.driveFoldersRepository.findOneBy({ name: 'Misskey', userId: job.data.user.id, parentId: folder.id }); + } + for await (const file of note.files) { const [filePath, cleanup] = await createTemp(); const slashdex = file.url.lastIndexOf('/'); const name = file.url.substring(slashdex + 1); - const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }); + const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: keyFolder?.id }); if (!exists) { try { @@ -301,6 +355,7 @@ export class ImportNotesProcessorService { user: user, path: filePath, name: name, + folderId: keyFolder?.id, }); files.push(driveFile); } else { @@ -316,28 +371,33 @@ export class ImportNotesProcessorService { } @bindThis - public async processMastoToDb(job: Bull.Job): Promise { + public async processMastoToDb(job: Bull.Job): Promise { const toot = job.data.target; const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { return; } + if (toot.directMessage) return; + const date = new Date(toot.object.published); let text = undefined; const files: MiDriveFile[] = []; let reply: MiNote | null = null; if (toot.object.inReplyTo != null) { - try { - reply = await this.apNoteService.resolveNote(toot.object.inReplyTo); - } catch (error) { - reply = null; + const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null; + if (parentNote) { + reply = parentNote; + } else { + try { + reply = await this.apNoteService.resolveNote(toot.object.inReplyTo); + } catch (error) { + reply = null; + } } } - if (toot.directMessage) return; - const hashtags = extractApHashtagObjects(toot.object.tag).map((x) => x.name).filter((x): x is string => x != null); try { @@ -357,32 +417,41 @@ export class ImportNotesProcessorService { } } - await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply }); + const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: toot.object.sensitive ? toot.object.summary : null, reply: reply }); + if (toot.childNotes) this.queueService.createImportMastoToDbJob(user, toot.childNotes, createdNote.id); } @bindThis - public async processPleroToDb(job: Bull.Job): Promise { + public async processPleroToDb(job: Bull.Job): Promise { const post = job.data.target; const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { return; } + if (post.directMessage) return; + const date = new Date(post.object.published); let text = undefined; const files: MiDriveFile[] = []; let reply: MiNote | null = null; + const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id }); + if (folder == null) return; + if (post.object.inReplyTo != null) { - try { - reply = await this.apNoteService.resolveNote(post.object.inReplyTo); - } catch (error) { - reply = null; + const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null; + if (parentNote) { + reply = parentNote; + } else { + try { + reply = await this.apNoteService.resolveNote(post.object.inReplyTo); + } catch (error) { + reply = null; + } } } - if (post.directMessage) return; - const hashtags = extractApHashtagObjects(post.object.tag).map((x) => x.name).filter((x): x is string => x != null); try { @@ -392,12 +461,18 @@ export class ImportNotesProcessorService { } if (post.object.attachment && this.isIterable(post.object.attachment)) { + let pleroFolder = await this.driveFoldersRepository.findOneBy({ name: 'Pleroma', userId: job.data.user.id, parentId: folder.id }); + if (pleroFolder == null) { + await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Pleroma', userId: job.data.user.id, parentId: folder.id }); + pleroFolder = await this.driveFoldersRepository.findOneBy({ name: 'Pleroma', userId: job.data.user.id, parentId: folder.id }); + } + for await (const file of post.object.attachment) { const slashdex = file.url.lastIndexOf('/'); const name = file.url.substring(slashdex + 1); const [filePath, cleanup] = await createTemp(); - const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }); + const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: pleroFolder?.id }); if (!exists) { try { @@ -409,6 +484,7 @@ export class ImportNotesProcessorService { user: user, path: filePath, name: name, + folderId: pleroFolder?.id, }); files.push(driveFile); } else { @@ -419,7 +495,8 @@ export class ImportNotesProcessorService { } } - await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: post.object.sensitive ? post.object.summary : null, reply: reply }); + const createdNote = await this.noteCreateService.import(user, { createdAt: date, text: text, files: files, apMentions: new Array(0), cw: post.object.sensitive ? post.object.summary : null, reply: reply }); + if (post.childNotes) this.queueService.createImportPleroToDbJob(user, post.childNotes, createdNote.id); } @bindThis @@ -468,13 +545,16 @@ export class ImportNotesProcessorService { } @bindThis - public async processTwitterDb(job: Bull.Job): Promise { + public async processTwitterDb(job: Bull.Job): Promise { const tweet = job.data.target; const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); if (user == null) { return; } + const folder = await this.driveFoldersRepository.findOneBy({ name: 'Imports', userId: job.data.user.id }); + if (folder == null) return; + const parentNote = job.data.note ? await this.notesRepository.findOneBy({ id: job.data.note }) : null; async function replaceTwitterUrls(full_text: string, urls: any) { @@ -500,13 +580,19 @@ export class ImportNotesProcessorService { const files: MiDriveFile[] = []; if (tweet.extended_entities && this.isIterable(tweet.extended_entities.media)) { + let twitFolder = await this.driveFoldersRepository.findOneBy({ name: 'Twitter', userId: job.data.user.id, parentId: folder.id }); + if (twitFolder == null) { + await this.driveFoldersRepository.insert({ id: this.idService.gen(), name: 'Twitter', userId: job.data.user.id, parentId: folder.id }); + twitFolder = await this.driveFoldersRepository.findOneBy({ name: 'Twitter', userId: job.data.user.id, parentId: folder.id }); + } + for await (const file of tweet.extended_entities.media) { if (file.video_info) { const [filePath, cleanup] = await createTemp(); const slashdex = file.video_info.variants[0].url.lastIndexOf('/'); const name = file.video_info.variants[0].url.substring(slashdex + 1); - const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }); + const exists = await this.driveFilesRepository.findOneBy({ name: name, userId: user.id }) ?? await this.driveFilesRepository.findOneBy({ name: name, userId: user.id, folderId: twitFolder?.id }); const videos = file.video_info.variants.filter((x: any) => x.content_type === 'video/mp4'); @@ -520,6 +606,7 @@ export class ImportNotesProcessorService { user: user, path: filePath, name: name, + folderId: twitFolder?.id, }); files.push(driveFile); } else { @@ -545,6 +632,7 @@ export class ImportNotesProcessorService { user: user, path: filePath, name: name, + folderId: twitFolder?.id, }); files.push(driveFile); } else { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 8d09e4e19..432b3d364 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -50,12 +50,12 @@ export type DbJobMap = { exportUserLists: DbJobDataWithUser; importAntennas: DBAntennaImportJobData; importNotes: DbNoteImportJobData; - importTweetsToDb: DbKeyNoteImportToDbJobData; + importTweetsToDb: DbNoteWithParentImportToDbJobData; importIGToDb: DbNoteImportToDbJobData; importFBToDb: DbNoteImportToDbJobData; - importMastoToDb: DbNoteImportToDbJobData; - importPleroToDb: DbNoteImportToDbJobData; - importKeyNotesToDb: DbKeyNoteImportToDbJobData; + importMastoToDb: DbNoteWithParentImportToDbJobData; + importPleroToDb: DbNoteWithParentImportToDbJobData; + importKeyNotesToDb: DbNoteWithParentImportToDbJobData; importFollowing: DbUserImportJobData; importFollowingToDb: DbUserImportToDbJobData; importMuting: DbUserImportJobData; @@ -113,7 +113,7 @@ export type DbNoteImportToDbJobData = { target: any; }; -export type DbKeyNoteImportToDbJobData = { +export type DbNoteWithParentImportToDbJobData = { user: ThinUser; target: any; note: MiNote['id'] | null; diff --git a/packages/backend/src/server/api/endpoints/i/import-notes.ts b/packages/backend/src/server/api/endpoints/i/import-notes.ts index 87b739f36..4e0016355 100644 --- a/packages/backend/src/server/api/endpoints/i/import-notes.ts +++ b/packages/backend/src/server/api/endpoints/i/import-notes.ts @@ -13,7 +13,7 @@ export const meta = { prohibitMoved: true, limit: { duration: ms('1hour'), - max: 5, + max: 2, }, errors: { diff --git a/packages/backend/src/server/api/endpoints/notes/edit.ts b/packages/backend/src/server/api/endpoints/notes/edit.ts index 6140c80a5..49fa4c3bd 100644 --- a/packages/backend/src/server/api/endpoints/notes/edit.ts +++ b/packages/backend/src/server/api/endpoints/notes/edit.ts @@ -14,7 +14,7 @@ import { DI } from '@/di-symbols.js'; import { ApiError } from '../../error.js'; export const meta = { - tags: ["notes"], + tags: ['notes'], requireCredential: true, @@ -23,99 +23,99 @@ export const meta = { max: 300, }, - kind: "write:notes", + kind: 'write:notes', res: { - type: "object", + type: 'object', optional: false, nullable: false, properties: { createdNote: { - type: "object", + type: 'object', optional: false, nullable: false, - ref: "Note", + ref: 'Note', }, }, }, errors: { noSuchRenoteTarget: { - message: "No such renote target.", - code: "NO_SUCH_RENOTE_TARGET", - id: "b5c90186-4ab0-49c8-9bba-a1f76c282ba4", + message: 'No such renote target.', + code: 'NO_SUCH_RENOTE_TARGET', + id: 'b5c90186-4ab0-49c8-9bba-a1f76c282ba4', }, cannotReRenote: { - message: "You can not Renote a pure Renote.", - code: "CANNOT_RENOTE_TO_A_PURE_RENOTE", - id: "fd4cc33e-2a37-48dd-99cc-9b806eb2031a", + message: 'You can not Renote a pure Renote.', + code: 'CANNOT_RENOTE_TO_A_PURE_RENOTE', + id: 'fd4cc33e-2a37-48dd-99cc-9b806eb2031a', }, noSuchReplyTarget: { - message: "No such reply target.", - code: "NO_SUCH_REPLY_TARGET", - id: "749ee0f6-d3da-459a-bf02-282e2da4292c", + message: 'No such reply target.', + code: 'NO_SUCH_REPLY_TARGET', + id: '749ee0f6-d3da-459a-bf02-282e2da4292c', }, cannotReplyToPureRenote: { - message: "You can not reply to a pure Renote.", - code: "CANNOT_REPLY_TO_A_PURE_RENOTE", - id: "3ac74a84-8fd5-4bb0-870f-01804f82ce15", + message: 'You can not reply to a pure Renote.', + code: 'CANNOT_REPLY_TO_A_PURE_RENOTE', + id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15', }, cannotCreateAlreadyExpiredPoll: { - message: "Poll is already expired.", - code: "CANNOT_CREATE_ALREADY_EXPIRED_POLL", - id: "04da457d-b083-4055-9082-955525eda5a5", + message: 'Poll is already expired.', + code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL', + id: '04da457d-b083-4055-9082-955525eda5a5', }, noSuchChannel: { - message: "No such channel.", - code: "NO_SUCH_CHANNEL", - id: "b1653923-5453-4edc-b786-7c4f39bb0bbb", + message: 'No such channel.', + code: 'NO_SUCH_CHANNEL', + id: 'b1653923-5453-4edc-b786-7c4f39bb0bbb', }, youHaveBeenBlocked: { - message: "You have been blocked by this user.", - code: "YOU_HAVE_BEEN_BLOCKED", - id: "b390d7e1-8a5e-46ed-b625-06271cafd3d3", + message: 'You have been blocked by this user.', + code: 'YOU_HAVE_BEEN_BLOCKED', + id: 'b390d7e1-8a5e-46ed-b625-06271cafd3d3', }, accountLocked: { - message: "You migrated. Your account is now locked.", - code: "ACCOUNT_LOCKED", - id: "d390d7e1-8a5e-46ed-b625-06271cafd3d3", + message: 'You migrated. Your account is now locked.', + code: 'ACCOUNT_LOCKED', + id: 'd390d7e1-8a5e-46ed-b625-06271cafd3d3', }, needsEditId: { - message: "You need to specify `editId`.", - code: "NEEDS_EDIT_ID", - id: "d697edc8-8c73-4de8-bded-35fd198b79e5", + message: 'You need to specify `editId`.', + code: 'NEEDS_EDIT_ID', + id: 'd697edc8-8c73-4de8-bded-35fd198b79e5', }, noSuchNote: { - message: "No such note.", - code: "NO_SUCH_NOTE", - id: "eef6c173-3010-4a23-8674-7c4fcaeba719", + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'eef6c173-3010-4a23-8674-7c4fcaeba719', }, youAreNotTheAuthor: { - message: "You are not the author of this note.", - code: "YOU_ARE_NOT_THE_AUTHOR", - id: "c6e61685-411d-43d0-b90a-a448d2539001", + message: 'You are not the author of this note.', + code: 'YOU_ARE_NOT_THE_AUTHOR', + id: 'c6e61685-411d-43d0-b90a-a448d2539001', }, cannotPrivateRenote: { - message: "You can not perform a private renote.", - code: "CANNOT_PRIVATE_RENOTE", - id: "19a50f1c-84fa-4e33-81d3-17834ccc0ad8", + message: 'You can not perform a private renote.', + code: 'CANNOT_PRIVATE_RENOTE', + id: '19a50f1c-84fa-4e33-81d3-17834ccc0ad8', }, notLocalUser: { - message: "You are not a local user.", - code: "NOT_LOCAL_USER", - id: "b907f407-2aa0-4283-800b-a2c56290b822", + message: 'You are not a local user.', + code: 'NOT_LOCAL_USER', + id: 'b907f407-2aa0-4283-800b-a2c56290b822', }, cannotRenoteOutsideOfChannel: { @@ -127,60 +127,63 @@ export const meta = { } as const; export const paramDef = { - type: "object", + type: 'object', properties: { - editId: { type: "string", format: "misskey:id" }, - visibility: { type: "string", enum: ['public', 'home', 'followers', 'specified'], default: "public" }, + editId: { type: 'string', format: 'misskey:id' }, + visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' }, visibleUserIds: { - type: "array", + type: 'array', uniqueItems: true, items: { - type: "string", - format: "misskey:id", + type: 'string', + format: 'misskey:id', }, }, - text: { type: "string", maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true }, - cw: { type: "string", nullable: true, minLength: 1, maxLength: 250 }, - localOnly: { type: "boolean", default: false }, - noExtractMentions: { type: "boolean", default: false }, - noExtractHashtags: { type: "boolean", default: false }, - noExtractEmojis: { type: "boolean", default: false }, + cw: { type: 'string', nullable: true, minLength: 1, maxLength: 250 }, + localOnly: { type: 'boolean', default: false }, + reactionAcceptance: { type: 'string', nullable: true, enum: [null, 'likeOnly', 'likeOnlyForRemote', 'nonSensitiveOnly', 'nonSensitiveOnlyForLocalLikeOnlyForRemote'], default: null }, + noExtractMentions: { type: 'boolean', default: false }, + noExtractHashtags: { type: 'boolean', default: false }, + noExtractEmojis: { type: 'boolean', default: false }, + replyId: { type: 'string', format: 'misskey:id', nullable: true }, + renoteId: { type: 'string', format: 'misskey:id', nullable: true }, + channelId: { type: 'string', format: 'misskey:id', nullable: true }, + text: { + type: 'string', + minLength: 1, + maxLength: MAX_NOTE_TEXT_LENGTH, + nullable: true, + }, fileIds: { - type: "array", + type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, - items: { type: "string", format: "misskey:id" }, + items: { type: 'string', format: 'misskey:id' }, }, mediaIds: { - deprecated: true, - description: - "Use `fileIds` instead. If both are specified, this property is discarded.", - type: "array", + type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, - items: { type: "string", format: "misskey:id" }, + items: { type: 'string', format: 'misskey:id' }, }, - replyId: { type: "string", format: "misskey:id", nullable: true }, - renoteId: { type: "string", format: "misskey:id", nullable: true }, - channelId: { type: "string", format: "misskey:id", nullable: true }, poll: { - type: "object", + type: 'object', nullable: true, properties: { choices: { - type: "array", + type: 'array', uniqueItems: true, minItems: 2, maxItems: 10, - items: { type: "string", minLength: 1, maxLength: 50 }, + items: { type: 'string', minLength: 1, maxLength: 50 }, }, - multiple: { type: "boolean", default: false }, - expiresAt: { type: "integer", nullable: true }, - expiredAfter: { type: "integer", nullable: true, minimum: 1 }, + multiple: { type: 'boolean' }, + expiresAt: { type: 'integer', nullable: true }, + expiredAfter: { type: 'integer', nullable: true, minimum: 1 }, }, - required: ["choices"], + required: ['choices'], }, }, anyOf: [ @@ -188,32 +191,32 @@ export const paramDef = { // (re)note with text, files and poll are optional properties: { text: { - type: "string", + type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false, }, }, - required: ["text"], + required: ['text'], }, { // (re)note with files, text and poll are optional - required: ["fileIds"], + required: ['fileIds'], }, { // (re)note with files, text and poll are optional - required: ["mediaIds"], + required: ['mediaIds'], }, { // (re)note with poll, text and files are optional properties: { - poll: { type: "object", nullable: false }, + poll: { type: 'object', nullable: false }, }, - required: ["poll"], + required: ['poll'], }, { // pure renote - required: ["renoteId"], + required: ['renoteId'], }, ], } as const; diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index eafcfdf8d..ea7bfd70b 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -139,7 +139,9 @@ export class ClientServerService { 'type': 'image/png', 'purpose': 'maskable', }, { - 'src': '/static-assets/splash.png', + // 空文字列の場合右辺を使いたいため + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + 'src': instance.app512IconUrl || '/static-assets/icons/512.png', 'sizes': '300x300', 'type': 'image/png', 'purpose': 'any', diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 71236e4c5..72ae8d64a 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -39,9 +39,7 @@ export async function mainBoot() { let reloadDialogShowing = false; stream.on('_disconnected_', async () => { - if (defaultStore.state.serverDisconnectedBehavior === 'reload') { - location.reload(); - } else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { + if (defaultStore.state.serverDisconnectedBehavior === 'dialog') { if (reloadDialogShowing) return; reloadDialogShowing = true; const { canceled } = await confirm({ diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue index 4d4205365..3aaf73683 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -57,6 +57,48 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages } } +.root + .root { + position: relative; + margin-inline: -20px 0; + box-shadow: -4px 0 0 var(--panel), -15px 0 15px var(--panel); + overflow: clip; + isolation: isolate; + + &::before { + content: ""; + position: absolute; + inset: 0; + background: var(--panel); + z-index: -1; + } + + &::after { + content: ""; + position: absolute; + inset: 0; + background: var(--panel); + z-index: -1; + background: inherit; + } + + span { + display: inline-block; + white-space: nowrap; + max-width: 3em; + mask: linear-gradient(to right, #000 20%, rgba(0, 0, 0, 0.4)); + } + + + .root { + margin-inline: -10px 0; + padding-inline-end: 0; + box-shadow: -4px 0 0 var(--panel); + + span { + display: none; + } + } +} + .icon { width: 1.5em; height: 1.5em; diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 6dedf4702..4da8f16df 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -127,9 +127,8 @@ SPDX-License-Identifier: AGPL-3.0-only ref="quoteButton" :class="$style.footerButton" class="_button" - :style="quoted ? 'color: var(--accent) !important;' : ''" v-on:click.stop - @mousedown="quoted ? undoQuote(appearNote) : quote()" + @mousedown="quote()" > @@ -226,7 +225,10 @@ const currentClip = inject | null>('currentClip', nul let note = $ref(deepClone(props.note)); function noteclick(id: string) { - router.push(`/notes/${id}`); + const selection = document.getSelection(); + if (selection?.toString().length === 0) { + router.push(`/notes/${id}`); + } } // plugin @@ -278,14 +280,13 @@ const isLong = shouldCollapsed(appearNote, urls ?? []); const collapsed = ref(appearNote.cw == null && isLong); const isDeleted = ref(false); const renoted = ref(false); -const quoted = ref(false); const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const translation = ref(null); const translating = ref(false); const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance); const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id)); let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null))); -const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null); +const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const keymap = { 'r': () => reply(true), @@ -364,15 +365,6 @@ if (!props.mock) { }).then((res) => { renoted.value = res.length > 0; }); - - os.api("notes/renotes", { - noteId: appearNote.id, - userId: $i.id, - limit: 1, - quote: true, - }).then((res) => { - quoted.value = res.length > 0; - }); } } @@ -467,7 +459,6 @@ function quote() { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - quoted.value = res.length > 0; os.toast(i18n.ts.quoted); }); }); @@ -490,7 +481,6 @@ function quote() { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - quoted.value = res.length > 0; os.toast(i18n.ts.quoted); }); }); @@ -603,26 +593,6 @@ function undoRenote(note) : void { } } -function undoQuote(note) : void { - if (props.mock) { - return; - } - os.api("notes/unrenote", { - noteId: note.id, - quote: true - }); - os.toast(i18n.ts.rmquote); - quoted.value = false; - - const el = quoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } -} - function onContextmenu(ev: MouseEvent): void { if (props.mock) { return; diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index fba81f58b..448758256 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -137,8 +137,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="quoteButton" class="_button" :class="$style.noteFooterButton" - :style="quoted ? 'color: var(--accent) !important;' : ''" - @mousedown="quoted ? undoQuote() : quote()" + @mousedown="quote()" > @@ -310,7 +309,6 @@ const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); const isDeleted = ref(false); const renoted = ref(false); -const quoted = ref(false); const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false); const translation = ref(null); const translating = ref(false); @@ -323,7 +321,7 @@ const conversation = ref([]); const replies = ref([]); const quotes = ref([]); const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id); -const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null); +const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); watch(() => props.expandAllCws, (expandAllCws) => { if (expandAllCws !== showContent.value) showContent.value = expandAllCws; @@ -337,15 +335,6 @@ if ($i) { }).then((res) => { renoted.value = res.length > 0; }); - - os.api("notes/renotes", { - noteId: appearNote.id, - userId: $i.id, - limit: 1, - quote: true, - }).then((res) => { - quoted.value = res.length > 0; - }); } const keymap = { @@ -511,7 +500,6 @@ function quote() { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - quoted.value = res.length > 0; os.toast(i18n.ts.quoted); }); }); @@ -534,7 +522,6 @@ function quote() { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - quoted.value = res.length > 0; os.toast(i18n.ts.quoted); }); }); @@ -625,23 +612,6 @@ function undoRenote() : void { } } -function undoQuote() : void { - os.api("notes/unrenote", { - noteId: appearNote.id, - quote: true - }); - os.toast(i18n.ts.rmquote); - quoted.value = false; - - const el = quoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } -} - function onContextmenu(ev: MouseEvent): void { const isLink = (el: HTMLElement) => { if (el.tagName === 'A') return true; diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index 0c51ad913..b1d4ed3f7 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only

- +

diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index f61fc608d..3e33c7aa6 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -41,8 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="quoteButton" class="_button" :class="$style.noteFooterButton" - :style="quoted ? 'color: var(--accent) !important;' : ''" - @mousedown="quoted ? undoQuote() : quote()" + @mousedown="quote()" > @@ -125,7 +124,6 @@ const translation = ref(null); const translating = ref(false); const isDeleted = ref(false); const renoted = ref(false); -const quoted = ref(false); const reactButton = shallowRef(); const renoteButton = shallowRef(); const quoteButton = shallowRef(); @@ -133,7 +131,7 @@ const menuButton = shallowRef(); const likeButton = shallowRef(); let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note); -const defaultLike = computed(() => defaultStore.state.like !== '❤️' ? defaultStore.state.like : null); +const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null); const isRenote = ( props.note.renote != null && @@ -156,15 +154,6 @@ if ($i) { }).then((res) => { renoted.value = res.length > 0; }); - - os.api("notes/renotes", { - noteId: appearNote.id, - userId: $i.id, - limit: 1, - quote: true, - }).then((res) => { - quoted.value = res.length > 0; - }); } function focus() { @@ -255,23 +244,6 @@ function undoRenote() : void { } } -function undoQuote() : void { - os.api("notes/unrenote", { - noteId: appearNote.id, - quote: true - }); - os.toast(i18n.ts.rmquote); - quoted.value = false; - - const el = quoteButton.value as HTMLElement | null | undefined; - if (el) { - const rect = el.getBoundingClientRect(); - const x = rect.left + (el.offsetWidth / 2); - const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); - } -} - let showContent = $ref(false); watch(() => props.expandAllCws, (expandAllCws) => { @@ -342,7 +314,6 @@ function quote() { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - quoted.value = res.length > 0; os.toast(i18n.ts.quoted); }); }); @@ -365,7 +336,6 @@ function quote() { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - quoted.value = res.length > 0; os.toast(i18n.ts.quoted); }); }); diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue index 2becabb41..641b17d06 100644 --- a/packages/frontend/src/components/MkPoll.vue +++ b/packages/frontend/src/components/MkPoll.vue @@ -17,6 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.t('_poll.totalVotes', { n: total }) }} + · + {{ i18n.ts._poll.multiple }} · {{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }} {{ i18n.ts._poll.voted }} @@ -78,12 +80,19 @@ const vote = async (id) => { pleaseLogin(); if (props.readOnly || closed.value || isVoted.value) return; - - const { canceled } = await os.confirm({ - type: 'question', - text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }), - }); - if (canceled) return; + if (!props.note.poll.multiple) { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('voteConfirm', { choice: props.note.poll.choices[id].text }), + }); + if (canceled) return; + } else { + const { canceled } = await os.confirm({ + type: 'question', + text: i18n.t('voteConfirmMulti', { choice: props.note.poll.choices[id].text }), + }); + if (canceled) return; + } await os.api('notes/polls/vote', { noteId: props.note.id, diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index f72e4ddfc..74038cd62 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -931,8 +931,8 @@ onMounted(() => { poll = { choices: init.poll.choices.map(x => x.text), multiple: init.poll.multiple, - expiresAt: init.poll.expiresAt, - expiredAfter: init.poll.expiredAfter, + expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null, + expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null, }; } visibility = init.visibility; diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue index 110644947..8a306d172 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: - +
RN: ... @@ -63,7 +63,10 @@ const props = defineProps<{ const router = useRouter(); function noteclick(id: string) { - router.push(`/notes/${id}`); + const selection = document.getSelection(); + if (selection?.toString().length === 0) { + router.push(`/notes/${id}`); + } } const parsed = $computed(() => props.note.text ? mfm.parse(props.note.text) : null); diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index 1ddae40b9..d958b325e 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -28,9 +28,20 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
{{ i18n.ts.noAccountDescription }}
+
+
+
+ +
+
+ + +
+
+
{{ i18n.ts.notes }}
@@ -221,6 +232,48 @@ onMounted(() => { border-bottom: solid 1px var(--divider); } +.fields { + font-size: 0.8em; + padding: 16px; + border-top: solid 1px var(--divider); + border-bottom: solid 1px var(--divider); +} + +.field { + display: flex; + padding: 0; + margin: 0; + + &:not(:last-child) { + margin-bottom: 8px; + } + + :deep(span) { + white-space: nowrap !important; + } +} + +.fieldvalue { + width: 70%; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + word-wrap: nowrap; + margin: 0; +} + +.fieldname { + width: 100px; + max-height: 45px; + overflow: hidden; + white-space: nowrap; + display: inline; + text-overflow: ellipsis; + font-weight: bold; + text-align: center; + padding-inline-end: 10px; +} + .mfm { display: -webkit-box; -webkit-line-clamp: 5; diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue index 95f8950fb..35300a3bf 100644 --- a/packages/frontend/src/components/MkWidgets.vue +++ b/packages/frontend/src/components/MkWidgets.vue @@ -65,6 +65,13 @@ const props = defineProps<{ edit: boolean; }>(); +// This will not be available for now as I don't think this is needed +// const notesSearchAvailable = (($i == null && instance.policies.canSearchNotes) || ($i != null && $i.policies.canSearchNotes)); +/* if (!notesSearchAvailable) { + const wid = widgetDefs.findIndex(widget => widget === 'search'); + widgetDefs.splice(wid, 1); +} */ + const emit = defineEmits<{ (ev: 'updateWidgets', widgets: Widget[]): void; (ev: 'addWidget', widget: Widget): void; diff --git a/packages/frontend/src/components/SkSearchResultWindow.vue b/packages/frontend/src/components/SkSearchResultWindow.vue new file mode 100644 index 000000000..5a0412685 --- /dev/null +++ b/packages/frontend/src/components/SkSearchResultWindow.vue @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 7d55353d9..d437ea998 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -82,7 +82,13 @@ export default function(props: MfmProps) { res.push(t); } res.shift(); - return res; + + // Don't wrap whitespaces in a span + if (text === ' ') { + return res; + } + + return h('span', res); } else { return [text.replace(/\n/g, ' ')]; } diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index 320ece4ef..75c8e7358 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -216,6 +216,7 @@ onUnmounted(() => { &.active { opacity: 1; + color: var(--accent); } &.animate { diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index ed191818e..96a2d8a30 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + Change diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue index a6257defe..bbfc8da48 100644 --- a/packages/frontend/src/pages/settings/general.vue +++ b/packages/frontend/src/pages/settings/general.vue @@ -166,7 +166,6 @@ SPDX-License-Identifier: AGPL-3.0-only
- diff --git a/packages/frontend/src/pages/settings/reaction.vue b/packages/frontend/src/pages/settings/reaction.vue index 74d1b108e..213e73b52 100644 --- a/packages/frontend/src/pages/settings/reaction.vue +++ b/packages/frontend/src/pages/settings/reaction.vue @@ -24,9 +24,10 @@ SPDX-License-Identifier: AGPL-3.0-only - - - + + + + {{ i18n.ts.notSet }}
Change Reset @@ -82,6 +83,7 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { deepClone } from '@/scripts/clone.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; let reactions = $ref(deepClone(defaultStore.state.reactions)); const like = $computed(defaultStore.makeGetterSetter('like')); @@ -91,6 +93,16 @@ const reactionPickerWidth = $computed(defaultStore.makeGetterSetter('reactionPic const reactionPickerHeight = $computed(defaultStore.makeGetterSetter('reactionPickerHeight')); const reactionPickerUseDrawerForMobile = $computed(defaultStore.makeGetterSetter('reactionPickerUseDrawerForMobile')); +async function reloadAsk() { + const { canceled } = await os.confirm({ + type: 'info', + text: i18n.ts.reloadToApplySetting, + }); + if (canceled) return; + + unisonReload(); +} + function save() { defaultStore.set('reactions', reactions); } @@ -134,13 +146,15 @@ function chooseEmoji(ev: MouseEvent) { function chooseNewLike(ev: MouseEvent) { os.pickEmoji(ev.currentTarget ?? ev.target, { showPinned: false, - }).then(emoji => { + }).then(async emoji => { defaultStore.set('like', emoji as string); + await reloadAsk(); }); } -function resetLike() { - defaultStore.set('like', '❤️'); +async function resetLike() { + defaultStore.set('like', null); + await reloadAsk(); } watch($$(reactions), () => { diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index c8942314b..41d0df1b7 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -176,7 +176,13 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; copyToClipboard(`${url}/${canonical}`); }, - }, { + }, ...(user.host ? [{ + icon: 'ph-share ph-bold ph-lg', + text: i18n.ts.openRemoteProfile, + action: () => { + open(`${user.uri}`, '_blank'); + }, + }] : []), { icon: 'ph-envelope ph-bold ph-lg', text: i18n.ts.sendMessage, action: () => { diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index a655d3542..7844ef0fb 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -112,7 +112,7 @@ export const defaultStore = markRaw(new Storage('base', { }, like: { where: 'account', - default: '❤️', + default: null as string | null, }, mutedAds: { where: 'account', @@ -188,7 +188,7 @@ export const defaultStore = markRaw(new Storage('base', { }, serverDisconnectedBehavior: { where: 'device', - default: 'quiet' as 'quiet' | 'reload' | 'dialog' | 'disabled', + default: 'disabled' as 'quiet' | 'dialog' | 'disabled', }, nsfw: { where: 'device', diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue index 46403ae3d..6e275c534 100644 --- a/packages/frontend/src/ui/classic.sidebar.vue +++ b/packages/frontend/src/ui/classic.sidebar.vue @@ -109,7 +109,8 @@ watch(defaultStore.reactiveState.menuDisplay, () => { $nav-icon-only-width: 78px; // TODO: どこかに集約したい $avatar-size: 32px; $avatar-margin: 8px; - + position: sticky; + top: 16px; padding: 0 16px; box-sizing: border-box; width: 260px; diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue index c4239fc9d..8f06f6601 100644 --- a/packages/frontend/src/ui/classic.vue +++ b/packages/frontend/src/ui/classic.vue @@ -253,9 +253,13 @@ onMounted(() => { } > .widgets { - //--panelBorder: none; + position: sticky; + top: 0; width: 300px; - padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px)); + height: 100%; + padding-top: 16px; + box-sizing: border-box; + overflow: auto; @media (max-width: $widgets-hide-threshold) { display: none; diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 0213f94f5..448f2f81f 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -324,7 +324,7 @@ $widgets-hide-threshold: 1090px; min-width: 0; overflow: auto; overflow-y: scroll; - overscroll-behavior: contain; + overscroll-behavior: unset; background: var(--bg); } diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue new file mode 100644 index 000000000..26cc72671 --- /dev/null +++ b/packages/frontend/src/widgets/WidgetSearch.vue @@ -0,0 +1,155 @@ + + + + + diff --git a/packages/frontend/src/widgets/index.ts b/packages/frontend/src/widgets/index.ts index 405c49ab0..ae3bd09c8 100644 --- a/packages/frontend/src/widgets/index.ts +++ b/packages/frontend/src/widgets/index.ts @@ -33,6 +33,7 @@ export default function(app: App) { app.component('WidgetAichan', defineAsyncComponent(() => import('./WidgetAichan.vue'))); app.component('WidgetUserList', defineAsyncComponent(() => import('./WidgetUserList.vue'))); app.component('WidgetClicker', defineAsyncComponent(() => import('./WidgetClicker.vue'))); + app.component('WidgetSearch', defineAsyncComponent(() => import('./WidgetSearch.vue'))); } export const widgets = [ @@ -63,4 +64,5 @@ export const widgets = [ 'aichan', 'userList', 'clicker', + 'search', ]; diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json index 0113abb6d..01c5c1147 100644 --- a/packages/megalodon/package.json +++ b/packages/megalodon/package.json @@ -63,7 +63,7 @@ "@types/parse-link-header": "^2.0.3", "@types/uuid": "^9.0.7", "@types/ws": "^8.5.10", - "axios": "1.5.0", + "axios": "1.6.0", "dayjs": "^1.11.10", "form-data": "^4.0.0", "https-proxy-agent": "^7.0.2", diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index ec9a3d45c..10b9dd5eb 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -217,6 +217,7 @@ export type Note = { clippedCount?: number; poll?: { expiresAt: DateString | null; + expiredAfter: DateString | null; multiple: boolean; choices: { isVoted: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e750d515..a7b26f03e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1034,8 +1034,8 @@ importers: specifier: ^8.5.10 version: 8.5.10 axios: - specifier: 1.5.0 - version: 1.5.0 + specifier: 1.6.0 + version: 1.6.0 dayjs: specifier: ^1.11.10 version: 1.11.10 @@ -2285,15 +2285,6 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.22.11): - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - dependencies: - '@babel/core': 7.22.11 - '@babel/helper-plugin-utils': 7.22.5 - dev: true - /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.3): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -7294,7 +7285,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.3.8(typescript@5.2.2) - vue-component-type-helpers: 1.8.22 + vue-component-type-helpers: 1.8.24 transitivePeerDependencies: - encoding - supports-color @@ -7940,10 +7931,6 @@ packages: '@types/unist': 2.0.6 dev: true - /@types/http-cache-semantics@4.0.1: - resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==} - dev: false - /@types/http-cache-semantics@4.0.4: resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -9198,7 +9185,7 @@ packages: requiresBuild: true dependencies: delegates: 1.0.0 - readable-stream: 3.6.0 + readable-stream: 3.6.2 dev: false /arg@5.0.2: @@ -9470,8 +9457,8 @@ packages: - debug dev: false - /axios@1.5.0: - resolution: {integrity: sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==} + /axios@1.6.0: + resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} dependencies: follow-redirects: 1.15.3(debug@4.3.4) form-data: 4.0.0 @@ -9501,24 +9488,6 @@ packages: '@babel/core': 7.22.11 dev: true - /babel-jest@29.7.0(@babel/core@7.22.11): - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - dependencies: - '@babel/core': 7.22.11 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.0 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.22.11) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - dev: true - /babel-jest@29.7.0(@babel/core@7.23.3): resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9596,26 +9565,6 @@ packages: - supports-color dev: true - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.22.11): - resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.11 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.22.11) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.22.11) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.22.11) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.22.11) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.22.11) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.22.11) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.22.11) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.22.11) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.22.11) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.22.11) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.22.11) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.22.11) - dev: true - /babel-preset-current-node-syntax@1.0.1(@babel/core@7.23.3): resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} peerDependencies: @@ -9636,17 +9585,6 @@ packages: '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.3) dev: true - /babel-preset-jest@29.6.3(@babel/core@7.22.11): - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.22.11 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.0.1(@babel/core@7.22.11) - dev: true - /babel-preset-jest@29.6.3(@babel/core@7.23.3): resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -9725,7 +9663,7 @@ packages: dependencies: buffer: 5.7.1 inherits: 2.0.4 - readable-stream: 3.6.0 + readable-stream: 3.6.2 /blob-util@2.0.2: resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} @@ -9992,10 +9930,10 @@ packages: resolution: {integrity: sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==} engines: {node: '>=14.16'} dependencies: - '@types/http-cache-semantics': 4.0.1 + '@types/http-cache-semantics': 4.0.4 get-stream: 6.0.1 http-cache-semantics: 4.1.1 - keyv: 4.5.2 + keyv: 4.5.4 mimic-response: 4.0.0 normalize-url: 8.0.0 responselike: 3.0.0 @@ -10504,7 +10442,7 @@ packages: crc-32: 1.2.2 crc32-stream: 5.0.0 normalize-path: 3.0.0 - readable-stream: 3.6.0 + readable-stream: 3.6.2 dev: false /compressible@2.0.18: @@ -10635,28 +10573,9 @@ packages: engines: {node: '>= 12.0.0'} dependencies: crc-32: 1.2.2 - readable-stream: 3.6.0 + readable-stream: 3.6.2 dev: false - /create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.9.4) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - dev: true - /create-jest@29.7.0(@types/node@20.9.1): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -11091,6 +11010,7 @@ packages: /deepmerge@4.2.2: resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} engines: {node: '>=0.10.0'} + dev: false /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} @@ -11314,7 +11234,7 @@ packages: dependencies: end-of-stream: 1.4.4 inherits: 2.0.4 - readable-stream: 2.3.7 + readable-stream: 2.3.8 stream-shift: 1.0.1 dev: true @@ -13951,10 +13871,10 @@ packages: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0 + create-jest: 29.7.0(@types/node@20.9.1) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.9.4) + jest-config: 29.7.0(@types/node@20.9.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -14005,14 +13925,14 @@ packages: ts-node: optional: true dependencies: - '@babel/core': 7.22.11 + '@babel/core': 7.23.3 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 '@types/node': 20.9.1 - babel-jest: 29.7.0(@babel/core@7.22.11) + babel-jest: 29.7.0(@babel/core@7.23.3) chalk: 4.1.2 - ci-info: 3.7.1 - deepmerge: 4.2.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 jest-circus: 29.7.0 @@ -14741,12 +14661,6 @@ packages: safe-buffer: 5.2.1 dev: false - /keyv@4.5.2: - resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==} - dependencies: - json-buffer: 3.0.1 - dev: false - /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -14800,7 +14714,7 @@ packages: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} dependencies: - readable-stream: 2.3.7 + readable-stream: 2.3.8 dev: false /leven@3.1.0: @@ -17504,17 +17418,6 @@ packages: type-fest: 0.6.0 dev: true - /readable-stream@2.3.7: - resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -17525,7 +17428,6 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 - dev: true /readable-stream@3.6.0: resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} @@ -17534,6 +17436,7 @@ packages: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + dev: false /readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} @@ -18995,7 +18898,7 @@ packages: /through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: - readable-stream: 2.3.7 + readable-stream: 2.3.8 xtend: 4.0.2 dev: true @@ -19956,8 +19859,8 @@ packages: /vscode-textmate@8.0.0: resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} - /vue-component-type-helpers@1.8.22: - resolution: {integrity: sha512-LK3wJHs3vJxHG292C8cnsRusgyC5SEZDCzDCD01mdE/AoREFMl2tzLRuzwyuEsOIz13tqgBcnvysN3Lxsa14Fw==} + /vue-component-type-helpers@1.8.24: + resolution: {integrity: sha512-lqWs/7fdRXoSBAlbouHBX+LNuaY6gI9xWW34m/ZIz9zVPYHEyw0b2/zaCBwlKx0NtKTeF/6pOpvrxVkh7nhIYg==} dev: true /vue-component-type-helpers@1.8.4: