diff --git a/CHANGELOG.md b/CHANGELOG.md index adc9891746..5e6fa2cf4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,26 @@ --> +## 13.11.1 + +### General +- チャンネルの投稿を過去までさかのぼれるように + +### Client +- PWA時の絵文字ピッカーの位置をホームバーに重ならないように調整 +- リスト管理の画面でリストが無限に読み込まれる問題を修正 +- 自分のクリップが無限に読み込まれる問題を修正 +- チャンネルのお気に入りが無限に読み込まれる問題を修正 +- さがすのローカルユーザー(ピンどめ)が無限に生成される問題を修正 +- チャンネルを新規作成できない問題を修正 +- ユーザープレビューが表示されない問題を修正 + +### Server +- 通知読み込みでエラーが発生する場合がある問題を修正 +- リアクションできないことがある問題を修正 +- IDをaid以外に設定している場合の問題を修正 +- 連合しているインスタンスについて予期せず配送が全て停止されることがある問題を修正 + ## 13.11.0 ### NOTE @@ -20,6 +40,7 @@ ### General - チャンネルをお気に入りに登録できるように + - タイムラインのアンテナ選択などでは、フォローしているアンテナの代わりにお気に入りしたアンテナが表示されるようになっています。チャンネルをお気に入りに登録するには、当該チャンネルのページ→概要→⭐️のボタンを押します。 - チャンネルにノートをピン留めできるように ### Client diff --git a/locales/de-DE.yml b/locales/de-DE.yml index aa56493930..ef1cea6e72 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -196,7 +196,7 @@ instanceInfo: "Instanzinformationen" statistics: "Statistiken" clearQueue: "Warteschlange leeren" clearQueueConfirmTitle: "Möchtest du die Warteschlange wirklich leeren?" -clearQueueConfirmText: "Hierdurch werden jegliche noch nicht gesendete Notizen nicht förderiert. Normalerweise wird dies nicht benötigt." +clearQueueConfirmText: "Hierdurch werden jegliche noch nicht gesendete Notizen nicht föderiert. Normalerweise wird dies nicht benötigt." clearCachedFiles: "Cache leeren" clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?" blockedInstances: "Blockierte Instanzen" @@ -1696,7 +1696,7 @@ _visibility: followersDescription: "Nur für Follower sichtbar" specified: "Direkt" specifiedDescription: "Nur für bestimmte Benutzer sichtbar" - disableFederation: "Deförderiert" + disableFederation: "Deföderieren" disableFederationDescription: "Nicht an andere Instanzen übertragen" _postForm: replyPlaceholder: "Dieser Notiz antworten …" diff --git a/locales/en-US.yml b/locales/en-US.yml index 4b8ec86224..1137199739 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -999,7 +999,6 @@ _accountMigration: moveFromLabel: "Account to move from:" moveFromDescription: "Create an alias for the account to move from on this account if you wish to transfer its followers. This has to be done before the transfer! Then, enter the account to move to in the following format: @person@instance.com" migrationConfirm: "Really migrate this account to {account}? Once started, this process cannot be stopped or taken back, and you will not be able to use this account in its original state anymore.\n\nAlso, confirm you've created an alias at the account to migrate to." - _achievements: earnedAt: "Unlocked at" _types: @@ -1697,7 +1696,7 @@ _visibility: followersDescription: "Make visible to your followers only" specified: "Direct" specifiedDescription: "Make visible for specified users only" - disableFederation: "Unfederated" + disableFederation: "Defederate" disableFederationDescription: "Don't transmit to other instances" _postForm: replyPlaceholder: "Reply to this note..." diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 01209b6fd4..f9b65488bb 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -170,7 +170,7 @@ proxyAccountDescription: "Un profilo proxy funziona come follower per i profili host: "Server remoto" selectUser: "Seleziona profilo" recipient: "Destinatario" -annotation: "Annotazione" +annotation: "Annotazione preventiva" federation: "Federazione" instances: "Istanza" registeredAt: "Registrato presso" @@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Disabilita quest'opzione se non utilizzi HTTPS per le objectStorageUseProxy: "Usa proxy" objectStorageUseProxyDesc: "Disabilita quest'opzione se non usi proxy per la connessione API." objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di caricare" +s3ForcePathStyleDesc: "L'attivazione di s3ForcePathStyle impone di specificare il nome del bucket come parte del percorso nell'URL anziché del nome host. Potrebbe tornare utile quando si utilizzano applicazioni come Minio." serverLogs: "Log del server" deleteAll: "Cancella cronologia" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" @@ -564,7 +565,7 @@ invisibleNote: "Nota invisibile" enableInfiniteScroll: "Abilita scorrimento infinito" visibility: "Visibilità" poll: "Sondaggio" -useCw: "Nascondere media" +useCw: "Content Warning" enablePlayer: "Visualizza" disablePlayer: "Chiudi" expandTweet: "Espandi tweet" @@ -579,7 +580,7 @@ plugins: "Estensioni" preferencesBackups: "Backup delle impostazioni" deck: "Deck" undeck: "Esci dal deck" -useBlurEffectForModal: "Utilizza effetto sfocatura per i modali" +useBlurEffectForModal: "Utilizza effetto sfocatura per le finestre modali" useFullReactionPicker: "Usa la totalità del pannello di reazioni" width: "Larghezza" height: "Altezza" @@ -814,7 +815,7 @@ translatedFrom: "Tradotto da {x}" accountDeletionInProgress: "È in corso l'eliminazione del profilo" usernameInfo: "Un nome per identificare univocamente il tuo profilo sull'istanza. Puoi utilizzare caratteri alfanumerici maiuscoli, minuscoli e il trattino basso (_). Non potrai cambiare nome utente in seguito." aiChanMode: "Modalità Ai" -keepCw: "Mantieni il CW" +keepCw: "Mantieni il Content Warning" pubSub: "Publish/Subscribe del profilo" lastCommunication: "La comunicazione più recente" resolved: "Risolto" @@ -919,6 +920,7 @@ pushNotificationNotSupported: "Il client o il server non supporta le notifiche p sendPushNotificationReadMessage: "Elimina le notifiche push dopo la relativa lettura" sendPushNotificationReadMessageCaption: "Se possibile, verrà mostrata brevemente una notifica con il testo \"{emptyPushNotificationMessage}\". Potrebbe influire negativamente sulla durata della batteria." windowMaximize: "Ingrandisci" +windowMinimize: "Contrai finestra" windowRestore: "Ripristina" caption: "Didascalia" loggedInAsBot: "Connessione come Bot" @@ -960,6 +962,9 @@ copyErrorInfo: "Copia le informazioni sull'errore" joinThisServer: "Registrati su questa istanza" exploreOtherServers: "Trova altre istanze" letsLookAtTimeline: "Sbircia la timeline" +disableFederationConfirm: "Vuoi davvero disattivare la federazione?" +disableFederationConfirmWarn: "Anche se defederate, le Note continueranno ad essere pubbliche, se non diversamente specificato. Di solito, non è necessario far questo." +disableFederationOk: "Disabilita federazione" invitationRequiredToRegister: "L'accesso a questa istanza è solo ad invito. Può registrarsi solo chi ha un codice fornito dall'amministrazione." emailNotSupported: "L'istanza non supporta l'invio di email" postToTheChannel: "Pubblica nel canale" @@ -984,6 +989,16 @@ enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate" showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note" largeNoteReactions: "Ingrandisci le reazioni" noteIdOrUrl: "ID della Nota o URL" +accountMigration: "Migrazione del profilo" +accountMoved: "Questo profilo ha migrato altrove:" +_accountMigration: + moveTo: "Migrare questo profilo verso un un altro" + moveToLabel: "Profilo verso cui migrare" + moveAccountDescription: "Questa attività è irreversibile! Innanzitutto, assicurati di aver creato, nella istanza di destinazione, un alias con l'indirizzo di questo profilo. Successivamente, indica qui il profilo di destinazione in questo modo: @persona@istanza.it" + moveFrom: "Migra un altro profilo dentro a questo" + moveFromLabel: "Profilo da cui migrare:" + moveFromDescription: "Se desideri spostare i profili follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@istanza.it" + migrationConfirm: "Vuoi davvero migrare questo profilo su {account}? L'azione è irreversibile e non potrai più utilizzare questo profilo nel suo stato originale.\nInoltre, assicurati di aver già creato un alias sull'account a cui ti stai trasferendo." _achievements: earnedAt: "Data di conseguimento" _types: @@ -1676,12 +1691,12 @@ _visibility: public: "Pubblica" publicDescription: "Visibile per tutti sul Fediverso" home: "Home" - homeDescription: "Visibile solo sulla timeline \"Home\"" + homeDescription: "Visibile solo sulla timeline locale" followers: "Follower" - followersDescription: "Visibile solo per i tuoi follower" + followersDescription: "Visibile solo ai tuoi follower" specified: "Nota diretta" specifiedDescription: "Visibile solo ai profili menzionati" - disableFederation: "Interrompi la federazione" + disableFederation: "Federazione disabilitata" disableFederationDescription: "Non spedire attività alle altre istanze remote" _postForm: replyPlaceholder: "Rispondi a questa nota..." diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 398dbcd5c0..81f99aace2 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -989,7 +989,16 @@ enableChartsForFederatedInstances: "リモートサーバーのチャートを showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" largeNoteReactions: "ノートのリアクションを大きする" noteIdOrUrl: "ノートIDかURL" +accountMigration: "アカウントのお引っ越し" accountMoved: "このユーザーはさらのアカウントに引っ越したで:" +_accountMigration: + moveTo: "このアカウントをさらのアカウントに引っ越すで" + moveToLabel: "引っ越し先のアカウント:" + moveAccountDescription: "この操作は戻されへんで。まず引っ越し先のアカウントでこのアカウントへのエイリアスが作れたか確認してきなはれや。エイリアスができてたら、引っ越し先のアカウントをこんな風に入力してくれへんか?:@person@instance.com" + moveFrom: "別のアカウントからこのアカウントに引っ越す" + moveFromLabel: "引っ越し元のアカウント:" + moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したかったら、ここでエイリアスを作っとく必要があるで。必ずお引っ越しを実行する前に作っとかなあかんで!引っ越し元のアカウントをこんな風に入力してくれへんか?:@person@instance.com" + migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃ~んと確認しーや?" _achievements: earnedAt: "貰った日ぃ" _types: diff --git a/locales/sv-SE.yml b/locales/sv-SE.yml index c5653d32ec..6369adb454 100644 --- a/locales/sv-SE.yml +++ b/locales/sv-SE.yml @@ -345,6 +345,7 @@ aboutMisskey: "Om Misskey" administrator: "Administratör" passwordLessLogin: "Lösenordsfri inloggning" passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey." +resetPassword: "Återställ Lösenord" newPasswordIs: "Det nya lösenordet är \"{password}\"" share: "Dela" enable: "Aktivera" @@ -362,6 +363,7 @@ smtpUser: "Användarnamn" smtpPass: "Lösenord" emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering" clearCache: "Rensa cache" +onlineUsersCount: "{n} användare är online" enabled: "Aktiverad" user: "Användare" global: "Global" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 563609d500..d9df760186 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -148,7 +148,7 @@ settingGuide: "推荐配置" cacheRemoteFiles: "缓存远程文件" cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" flagAsBot: "这是一个机器人账号" -flagAsBotDescription: "如果此帐户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让Misskey的内部系统将此帐户识别为机器人。" +flagAsBotDescription: "如果此账户由程序控制,请启用此项。启用后,此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为,并让Misskey的内部系统将此账户识别为机器人。" flagAsCat: "将这个账户设定为一只猫" flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后,会在您的头像上出现猫耳朵,并将你的帖子中的「na」替换为「nya」,日文同理。" flagShowTimelineReplies: "在时间线上显示帖子的回复" @@ -989,6 +989,16 @@ enableChartsForFederatedInstances: "生成远程服务器的图表" showClipButtonInNoteFooter: "在贴文下方显示便签按钮" largeNoteReactions: "使用大图标来显示回应" noteIdOrUrl: "帖子ID或URL" +accountMigration: "账户迁移" +accountMoved: "此用户已迁移账户" +_accountMigration: + moveTo: "把这个账户迁移到新的账户" + moveToLabel: "迁移后的账户" + moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com" + moveFrom: "从别的账号迁移到此账户" + moveFromLabel: "迁移前的账户" + moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com" + migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时,请确认迁移后的账户,已创造别名。" _achievements: earnedAt: "达成时间" _types: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 69f3d2f52c..48934463d0 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -984,6 +984,10 @@ enableChartsForFederatedInstances: "生成遠端伺服器的圖表" showClipButtonInNoteFooter: "將摘錄添加至貼文" largeNoteReactions: "將貼文的反應放大顯示" noteIdOrUrl: "貼文ID或URL" +accountMigration: "遷移帳戶" +_accountMigration: + moveTo: "將這個帳戶遷移至新的帳戶" + moveToLabel: "要遷移的帳戶:" _achievements: earnedAt: "獲得日期" _types: diff --git a/package.json b/package.json index c590388f3d..89733ea179 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "13.11.0", + "version": "13.11.1", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 604a94707f..dc365986fe 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -44,7 +44,11 @@ export class CustomEmojiService { memoryCacheLifetime: 1000 * 60 * 3, // 3m fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), toRedisConverter: (value) => JSON.stringify(value.values()), - fromRedisConverter: (value) => new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])), // TODO: Date型の変換 + fromRedisConverter: (value) => { + // 原因不明だが配列以外が入ってくることがあるため + if (!Array.isArray(JSON.parse(value))) return undefined; + return new Map(JSON.parse(value).map((x: Emoji) => [x.name, x])); + }, // TODO: Date型の変換 }); } diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts index 1d0c87280f..56660ae0d0 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -29,6 +29,7 @@ export class FederatedInstanceService { toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => { const parsed = JSON.parse(value); + if (parsed == null) return null; return { ...parsed, firstRetrievedAt: new Date(parsed.firstRetrievedAt), diff --git a/packages/backend/src/core/IdService.ts b/packages/backend/src/core/IdService.ts index 94084ad84f..8aa6ccfc4e 100644 --- a/packages/backend/src/core/IdService.ts +++ b/packages/backend/src/core/IdService.ts @@ -3,10 +3,11 @@ import { ulid } from 'ulid'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { genAid, parseAid } from '@/misc/id/aid.js'; -import { genMeid } from '@/misc/id/meid.js'; -import { genMeidg } from '@/misc/id/meidg.js'; +import { genMeid, parseMeid } from '@/misc/id/meid.js'; +import { genMeidg, parseMeidg } from '@/misc/id/meidg.js'; import { genObjectId } from '@/misc/id/object-id.js'; import { bindThis } from '@/decorators.js'; +import { parseUlid } from '@/misc/id/ulid.js'; @Injectable() export class IdService { @@ -37,11 +38,10 @@ export class IdService { public parse(id: string): { date: Date; } { switch (this.method) { case 'aid': return parseAid(id); - // TODO - //case 'meid': - //case 'meidg': - //case 'ulid': - //case 'objectid': + case 'objectid': + case 'meid': return parseMeid(id); + case 'meidg': return parseMeidg(id); + case 'ulid': return parseUlid(id); default: throw new Error('unrecognized id generation method'); } } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 70a6d32fe2..62a2a33a19 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -186,7 +186,7 @@ class DeliverManager { for (const following of followers) { const inbox = following.followerSharedInbox ?? following.followerInbox; - inboxes.set(inbox, following.followerSharedInbox === null); + inboxes.set(inbox, following.followerSharedInbox != null); } } diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index d35414acf7..a4abd4f878 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -8,7 +8,7 @@ export class RedisKVCache { private memoryCache: MemoryKVCache; private fetcher: (key: string) => Promise; private toRedisConverter: (value: T) => string; - private fromRedisConverter: (value: string) => T; + private fromRedisConverter: (value: string) => T | undefined; // undefined means no cache constructor(redisClient: RedisKVCache['redisClient'], name: RedisKVCache['name'], opts: { lifetime: RedisKVCache['lifetime']; @@ -92,7 +92,7 @@ export class RedisSingleCache { private memoryCache: MemorySingleCache; private fetcher: () => Promise; private toRedisConverter: (value: T) => string; - private fromRedisConverter: (value: string) => T; + private fromRedisConverter: (value: string) => T | undefined; // undefined means no cache constructor(redisClient: RedisSingleCache['redisClient'], name: RedisSingleCache['name'], opts: { lifetime: RedisSingleCache['lifetime']; diff --git a/packages/backend/src/misc/id/aid.ts b/packages/backend/src/misc/id/aid.ts index 93a9929aa7..9e206ee98f 100644 --- a/packages/backend/src/misc/id/aid.ts +++ b/packages/backend/src/misc/id/aid.ts @@ -3,6 +3,8 @@ import * as crypto from 'node:crypto'; +export const aidRegExp = /^[0-9a-z]{10}$/; + const TIME2000 = 946684800000; let counter = crypto.randomBytes(2).readUInt16LE(0); diff --git a/packages/backend/src/misc/id/meid.ts b/packages/backend/src/misc/id/meid.ts index 30bbdf1698..337416b059 100644 --- a/packages/backend/src/misc/id/meid.ts +++ b/packages/backend/src/misc/id/meid.ts @@ -1,5 +1,8 @@ const CHARS = '0123456789abcdef'; +// same as object-id +export const meidRegExp = /^[0-9a-f]{24}$/; + function getTime(time: number) { if (time < 0) time = 0; if (time === 0) { @@ -24,3 +27,9 @@ function getRandom() { export function genMeid(date: Date): string { return getTime(date.getTime()) + getRandom(); } + +export function parseMeid(id: string): { date: Date; } { + return { + date: new Date(parseInt(id.slice(0, 12), 16) - 0x800000000000), + }; +} diff --git a/packages/backend/src/misc/id/meidg.ts b/packages/backend/src/misc/id/meidg.ts index d4aaaea1ba..19d0bc1fd2 100644 --- a/packages/backend/src/misc/id/meidg.ts +++ b/packages/backend/src/misc/id/meidg.ts @@ -3,6 +3,7 @@ const CHARS = '0123456789abcdef'; // 4bit Fixed hex value 'g' // 44bit UNIX Time ms in Hex // 48bit Random value in Hex +export const meidgRegExp = /^g[0-9a-f]{23}$/; function getTime(time: number) { if (time < 0) time = 0; @@ -26,3 +27,9 @@ function getRandom() { export function genMeidg(date: Date): string { return 'g' + getTime(date.getTime()) + getRandom(); } + +export function parseMeidg(id: string): { date: Date; } { + return { + date: new Date(parseInt(id.slice(1, 12), 16)), + }; +} diff --git a/packages/backend/src/misc/id/object-id.ts b/packages/backend/src/misc/id/object-id.ts index 392ea43301..aec3447bd7 100644 --- a/packages/backend/src/misc/id/object-id.ts +++ b/packages/backend/src/misc/id/object-id.ts @@ -1,5 +1,8 @@ const CHARS = '0123456789abcdef'; +// same as meid +export const objectIdRegExp = /^[0-9a-f]{24}$/; + function getTime(time: number) { if (time < 0) time = 0; if (time === 0) { @@ -24,3 +27,9 @@ function getRandom() { export function genObjectId(date: Date): string { return getTime(date.getTime()) + getRandom(); } + +export function parseObjectId(id: string): { date: Date; } { + return { + date: new Date(parseInt(id.slice(0, 8), 16) * 1000), + }; +} diff --git a/packages/backend/src/misc/id/ulid.ts b/packages/backend/src/misc/id/ulid.ts new file mode 100644 index 0000000000..e8aa752890 --- /dev/null +++ b/packages/backend/src/misc/id/ulid.ts @@ -0,0 +1,14 @@ +// Crockford's Base32 +// https://github.com/ulid/spec#encoding +const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; + +export const ulidRegExp = /^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$/; + +export function parseUlid(id: string): { date: Date; } { + const timestamp = id.slice(0, 10); + let time = 0; + for (let i = 0; i < 10; i++) { + time = time * 32 + CHARS.indexOf(timestamp[i]); + } + return { date: new Date(time) }; +} diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 2556557b24..2491d14235 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -75,13 +75,19 @@ export default class extends Endpoint { let timeline: Note[] = []; - const noteIdsRes = await this.redisClient.xrevrange( - `channelTimeline:${channel.id}`, - ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+', - '-', - 'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1 + const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1 + let noteIdsRes: [string, string[]][] = []; + + if (!ps.sinceId && !ps.sinceDate) { + noteIdsRes = await this.redisClient.xrevrange( + `channelTimeline:${channel.id}`, + ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : ps.untilDate ?? '+', + '-', + 'COUNT', limit); + } - if (noteIdsRes.length === 0) { + // redis から取得していないとき・取得数が足りないとき + if (noteIdsRes.length < limit) { //#region Construct query const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) .andWhere('note.channelId = :channelId', { channelId: channel.id }) diff --git a/packages/backend/test/unit/misc/id.ts b/packages/backend/test/unit/misc/id.ts new file mode 100644 index 0000000000..ecd0e60a31 --- /dev/null +++ b/packages/backend/test/unit/misc/id.ts @@ -0,0 +1,44 @@ +import { aidRegExp, genAid, parseAid } from '@/misc/id/aid.js'; +import { genMeid, meidRegExp, parseMeid } from '@/misc/id/meid.js'; +import { genMeidg, meidgRegExp, parseMeidg } from '@/misc/id/meidg.js'; +import { genObjectId, objectIdRegExp, parseObjectId } from '@/misc/id/object-id.js'; +import { ulidRegExp, parseUlid } from '@/misc/id/ulid.js'; +import { ulid } from 'ulid'; +import { describe, test, expect } from '@jest/globals'; + +describe('misc:id', () => { + test('aid', () => { + const date = new Date(); + const gotAid = genAid(date); + expect(gotAid).toMatch(aidRegExp); + expect(parseAid(gotAid).date.getTime()).toBe(date.getTime()); + }); + + test('meid', () => { + const date = new Date(); + const gotMeid = genMeid(date); + expect(gotMeid).toMatch(meidRegExp); + expect(parseMeid(gotMeid).date.getTime()).toBe(date.getTime()); + }); + + test('meidg', () => { + const date = new Date(); + const gotMeidg = genMeidg(date); + expect(gotMeidg).toMatch(meidgRegExp); + expect(parseMeidg(gotMeidg).date.getTime()).toBe(date.getTime()); + }); + + test('objectid', () => { + const date = new Date(); + const gotObjectId = genObjectId(date); + expect(gotObjectId).toMatch(objectIdRegExp); + expect(Math.floor(parseObjectId(gotObjectId).date.getTime() / 1000)).toBe(Math.floor(date.getTime() / 1000)); + }); + + test('ulid', () => { + const date = new Date(); + const gotUlid = ulid(date.getTime()); + expect(gotUlid).toMatch(ulidRegExp); + expect(parseUlid(gotUlid).date.getTime()).toBe(date.getTime()); + }); +}); diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index a5a39108d6..0ae182ce32 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -439,6 +439,7 @@ defineExpose({ &.asDrawer { width: 100% !important; + padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0; > .emojis { ::v-deep(section) { diff --git a/packages/frontend/src/directives/user-preview.ts b/packages/frontend/src/directives/user-preview.ts index ae12f2670a..397f449bd1 100644 --- a/packages/frontend/src/directives/user-preview.ts +++ b/packages/frontend/src/directives/user-preview.ts @@ -13,8 +13,6 @@ export class UserPreview { this.el = el; this.user = user; - this.attach(); - this.show = this.show.bind(this); this.close = this.close.bind(this); this.onMouseover = this.onMouseover.bind(this); @@ -22,6 +20,8 @@ export class UserPreview { this.onClick = this.onClick.bind(this); this.attach = this.attach.bind(this); this.detach = this.detach.bind(this); + + this.attach(); } private show() { diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 667caab966..9cb440d2bb 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -2,7 +2,7 @@ -
+
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue index fd1d2d03cf..3a5aa00c5b 100644 --- a/packages/frontend/src/pages/channels.vue +++ b/packages/frontend/src/pages/channels.vue @@ -47,6 +47,7 @@ const featuredPagination = { const favoritesPagination = { endpoint: 'channels/my-favorites' as const, limit: 100, + noPaging: true, }; const followingPagination = { endpoint: 'channels/followed' as const, diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue index c441407d97..3f4ff5182b 100644 --- a/packages/frontend/src/pages/explore.users.vue +++ b/packages/frontend/src/pages/explore.users.vue @@ -88,7 +88,7 @@ const tagUsers = $computed(() => ({ }, })); -const pinnedUsers = { endpoint: 'pinned-users' }; +const pinnedUsers = { endpoint: 'pinned-users', noPaging: true }; const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: { state: 'alive', origin: 'local', diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue index aad914d6bb..ccffa7b563 100644 --- a/packages/frontend/src/pages/my-clips/index.vue +++ b/packages/frontend/src/pages/my-clips/index.vue @@ -32,6 +32,7 @@ import { clipsCache } from '@/cache'; const pagination = { endpoint: 'clips/list' as const, + noPaging: true, limit: 10, }; diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue index 11a2aca8c5..47437f3e57 100644 --- a/packages/frontend/src/pages/my-lists/index.vue +++ b/packages/frontend/src/pages/my-lists/index.vue @@ -30,6 +30,7 @@ const pagingComponent = $shallowRef>(); const pagination = { endpoint: 'users/lists/list' as const, + noPaging: true, limit: 10, };