Merge pull request #10543 from misskey-dev/develop

Release: 13.11.1
This commit is contained in:
syuilo 2023-04-09 10:29:36 +09:00 committed by GitHub
commit 8b1362ab03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 194 additions and 34 deletions

View file

@ -12,6 +12,26 @@
--> -->
## 13.11.1
### General
- チャンネルの投稿を過去までさかのぼれるように
### Client
- PWA時の絵文字ピッカーの位置をホームバーに重ならないように調整
- リスト管理の画面でリストが無限に読み込まれる問題を修正
- 自分のクリップが無限に読み込まれる問題を修正
- チャンネルのお気に入りが無限に読み込まれる問題を修正
- さがすのローカルユーザー(ピンどめ)が無限に生成される問題を修正
- チャンネルを新規作成できない問題を修正
- ユーザープレビューが表示されない問題を修正
### Server
- 通知読み込みでエラーが発生する場合がある問題を修正
- リアクションできないことがある問題を修正
- IDをaid以外に設定している場合の問題を修正
- 連合しているインスタンスについて予期せず配送が全て停止されることがある問題を修正
## 13.11.0 ## 13.11.0
### NOTE ### NOTE
@ -20,6 +40,7 @@
### General ### General
- チャンネルをお気に入りに登録できるように - チャンネルをお気に入りに登録できるように
- タイムラインのアンテナ選択などでは、フォローしているアンテナの代わりにお気に入りしたアンテナが表示されるようになっています。チャンネルをお気に入りに登録するには、当該チャンネルのページ→概要→⭐️のボタンを押します。
- チャンネルにノートをピン留めできるように - チャンネルにノートをピン留めできるように
### Client ### Client

View file

@ -196,7 +196,7 @@ instanceInfo: "Instanzinformationen"
statistics: "Statistiken" statistics: "Statistiken"
clearQueue: "Warteschlange leeren" clearQueue: "Warteschlange leeren"
clearQueueConfirmTitle: "Möchtest du die Warteschlange wirklich 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" clearCachedFiles: "Cache leeren"
clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?" clearCachedFilesConfirm: "Sollen alle im Cache gespeicherten Dateien von anderen Instanzen wirklich gelöscht werden?"
blockedInstances: "Blockierte Instanzen" blockedInstances: "Blockierte Instanzen"
@ -1696,7 +1696,7 @@ _visibility:
followersDescription: "Nur für Follower sichtbar" followersDescription: "Nur für Follower sichtbar"
specified: "Direkt" specified: "Direkt"
specifiedDescription: "Nur für bestimmte Benutzer sichtbar" specifiedDescription: "Nur für bestimmte Benutzer sichtbar"
disableFederation: "Deförderiert" disableFederation: "Deföderieren"
disableFederationDescription: "Nicht an andere Instanzen übertragen" disableFederationDescription: "Nicht an andere Instanzen übertragen"
_postForm: _postForm:
replyPlaceholder: "Dieser Notiz antworten …" replyPlaceholder: "Dieser Notiz antworten …"

View file

@ -999,7 +999,6 @@ _accountMigration:
moveFromLabel: "Account to move from:" 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" 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." 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: _achievements:
earnedAt: "Unlocked at" earnedAt: "Unlocked at"
_types: _types:
@ -1697,7 +1696,7 @@ _visibility:
followersDescription: "Make visible to your followers only" followersDescription: "Make visible to your followers only"
specified: "Direct" specified: "Direct"
specifiedDescription: "Make visible for specified users only" specifiedDescription: "Make visible for specified users only"
disableFederation: "Unfederated" disableFederation: "Defederate"
disableFederationDescription: "Don't transmit to other instances" disableFederationDescription: "Don't transmit to other instances"
_postForm: _postForm:
replyPlaceholder: "Reply to this note..." replyPlaceholder: "Reply to this note..."

View file

@ -170,7 +170,7 @@ proxyAccountDescription: "Un profilo proxy funziona come follower per i profili
host: "Server remoto" host: "Server remoto"
selectUser: "Seleziona profilo" selectUser: "Seleziona profilo"
recipient: "Destinatario" recipient: "Destinatario"
annotation: "Annotazione" annotation: "Annotazione preventiva"
federation: "Federazione" federation: "Federazione"
instances: "Istanza" instances: "Istanza"
registeredAt: "Registrato presso" registeredAt: "Registrato presso"
@ -506,6 +506,7 @@ objectStorageUseSSLDesc: "Disabilita quest'opzione se non utilizzi HTTPS per le
objectStorageUseProxy: "Usa proxy" objectStorageUseProxy: "Usa proxy"
objectStorageUseProxyDesc: "Disabilita quest'opzione se non usi proxy per la connessione API." objectStorageUseProxyDesc: "Disabilita quest'opzione se non usi proxy per la connessione API."
objectStorageSetPublicRead: "Imposta \"visibilità pubblica\" al momento di caricare" 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" serverLogs: "Log del server"
deleteAll: "Cancella cronologia" deleteAll: "Cancella cronologia"
showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline" showFixedPostForm: "Visualizzare la finestra di pubblicazione in cima alla timeline"
@ -564,7 +565,7 @@ invisibleNote: "Nota invisibile"
enableInfiniteScroll: "Abilita scorrimento infinito" enableInfiniteScroll: "Abilita scorrimento infinito"
visibility: "Visibilità" visibility: "Visibilità"
poll: "Sondaggio" poll: "Sondaggio"
useCw: "Nascondere media" useCw: "Content Warning"
enablePlayer: "Visualizza" enablePlayer: "Visualizza"
disablePlayer: "Chiudi" disablePlayer: "Chiudi"
expandTweet: "Espandi tweet" expandTweet: "Espandi tweet"
@ -579,7 +580,7 @@ plugins: "Estensioni"
preferencesBackups: "Backup delle impostazioni" preferencesBackups: "Backup delle impostazioni"
deck: "Deck" deck: "Deck"
undeck: "Esci dal 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" useFullReactionPicker: "Usa la totalità del pannello di reazioni"
width: "Larghezza" width: "Larghezza"
height: "Altezza" height: "Altezza"
@ -814,7 +815,7 @@ translatedFrom: "Tradotto da {x}"
accountDeletionInProgress: "È in corso l'eliminazione del profilo" 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." 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" aiChanMode: "Modalità Ai"
keepCw: "Mantieni il CW" keepCw: "Mantieni il Content Warning"
pubSub: "Publish/Subscribe del profilo" pubSub: "Publish/Subscribe del profilo"
lastCommunication: "La comunicazione più recente" lastCommunication: "La comunicazione più recente"
resolved: "Risolto" 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" 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." sendPushNotificationReadMessageCaption: "Se possibile, verrà mostrata brevemente una notifica con il testo \"{emptyPushNotificationMessage}\". Potrebbe influire negativamente sulla durata della batteria."
windowMaximize: "Ingrandisci" windowMaximize: "Ingrandisci"
windowMinimize: "Contrai finestra"
windowRestore: "Ripristina" windowRestore: "Ripristina"
caption: "Didascalia" caption: "Didascalia"
loggedInAsBot: "Connessione come Bot" loggedInAsBot: "Connessione come Bot"
@ -960,6 +962,9 @@ copyErrorInfo: "Copia le informazioni sull'errore"
joinThisServer: "Registrati su questa istanza" joinThisServer: "Registrati su questa istanza"
exploreOtherServers: "Trova altre istanze" exploreOtherServers: "Trova altre istanze"
letsLookAtTimeline: "Sbircia la timeline" 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." 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" emailNotSupported: "L'istanza non supporta l'invio di email"
postToTheChannel: "Pubblica nel canale" 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" showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note"
largeNoteReactions: "Ingrandisci le reazioni" largeNoteReactions: "Ingrandisci le reazioni"
noteIdOrUrl: "ID della Nota o URL" 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: _achievements:
earnedAt: "Data di conseguimento" earnedAt: "Data di conseguimento"
_types: _types:
@ -1676,12 +1691,12 @@ _visibility:
public: "Pubblica" public: "Pubblica"
publicDescription: "Visibile per tutti sul Fediverso" publicDescription: "Visibile per tutti sul Fediverso"
home: "Home" home: "Home"
homeDescription: "Visibile solo sulla timeline \"Home\"" homeDescription: "Visibile solo sulla timeline locale"
followers: "Follower" followers: "Follower"
followersDescription: "Visibile solo per i tuoi follower" followersDescription: "Visibile solo ai tuoi follower"
specified: "Nota diretta" specified: "Nota diretta"
specifiedDescription: "Visibile solo ai profili menzionati" specifiedDescription: "Visibile solo ai profili menzionati"
disableFederation: "Interrompi la federazione" disableFederation: "Federazione disabilitata"
disableFederationDescription: "Non spedire attività alle altre istanze remote" disableFederationDescription: "Non spedire attività alle altre istanze remote"
_postForm: _postForm:
replyPlaceholder: "Rispondi a questa nota..." replyPlaceholder: "Rispondi a questa nota..."

View file

@ -989,7 +989,16 @@ enableChartsForFederatedInstances: "リモートサーバーのチャートを
showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加"
largeNoteReactions: "ノートのリアクションを大きする" largeNoteReactions: "ノートのリアクションを大きする"
noteIdOrUrl: "ートIDかURL" noteIdOrUrl: "ートIDかURL"
accountMigration: "アカウントのお引っ越し"
accountMoved: "このユーザーはさらのアカウントに引っ越したで:" accountMoved: "このユーザーはさらのアカウントに引っ越したで:"
_accountMigration:
moveTo: "このアカウントをさらのアカウントに引っ越すで"
moveToLabel: "引っ越し先のアカウント:"
moveAccountDescription: "この操作は戻されへんで。まず引っ越し先のアカウントでこのアカウントへのエイリアスが作れたか確認してきなはれや。エイリアスができてたら、引っ越し先のアカウントをこんな風に入力してくれへんか?:@person@instance.com"
moveFrom: "別のアカウントからこのアカウントに引っ越す"
moveFromLabel: "引っ越し元のアカウント:"
moveFromDescription: "別のアカウントからこのアカウントにフォロワーを引き継いで引っ越したかったら、ここでエイリアスを作っとく必要があるで。必ずお引っ越しを実行する前に作っとかなあかんで!引っ越し元のアカウントをこんな風に入力してくれへんか?:@person@instance.com"
migrationConfirm: "ほんまにこのアカウントを {account} に引っ越すんか?一回引っ越してもうたら取り消されへんし、二度とこのアカウントを元に戻されへんくなるで。\nそれと、引っ越し先のアカウントでエイリアスが作れたかちゃんと確認しーや"
_achievements: _achievements:
earnedAt: "貰った日ぃ" earnedAt: "貰った日ぃ"
_types: _types:

View file

@ -345,6 +345,7 @@ aboutMisskey: "Om Misskey"
administrator: "Administratör" administrator: "Administratör"
passwordLessLogin: "Lösenordsfri inloggning" passwordLessLogin: "Lösenordsfri inloggning"
passwordLessLoginDescription: "Tillåter lösenordsfri inloggning med endast en säkerhetsnyckel eller en passkey." 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}\"" newPasswordIs: "Det nya lösenordet är \"{password}\""
share: "Dela" share: "Dela"
enable: "Aktivera" enable: "Aktivera"
@ -362,6 +363,7 @@ smtpUser: "Användarnamn"
smtpPass: "Lösenord" smtpPass: "Lösenord"
emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering" emptyToDisableSmtpAuth: "Lämna användarnamn och lösenord tomt för att avaktivera SMTP verifiering"
clearCache: "Rensa cache" clearCache: "Rensa cache"
onlineUsersCount: "{n} användare är online"
enabled: "Aktiverad" enabled: "Aktiverad"
user: "Användare" user: "Användare"
global: "Global" global: "Global"

View file

@ -148,7 +148,7 @@ settingGuide: "推荐配置"
cacheRemoteFiles: "缓存远程文件" cacheRemoteFiles: "缓存远程文件"
cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。" cacheRemoteFilesDescription: "当禁用此设定时远程文件将直接从远程服务器载入。禁用后会减小储存空间需求,但是会增加流量,因为缩略图不会被生成。"
flagAsBot: "这是一个机器人账号" flagAsBot: "这是一个机器人账号"
flagAsBotDescription: "如果此户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此户识别为机器人。" flagAsBotDescription: "如果此户由程序控制请启用此项。启用后此标志可以帮助其他开发人员防止机器人之间产生无限互动的行为并让Misskey的内部系统将此户识别为机器人。"
flagAsCat: "将这个账户设定为一只猫" flagAsCat: "将这个账户设定为一只猫"
flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后会在您的头像上出现猫耳朵并将你的帖子中的「na」替换为「nya」日文同理。" flagAsCatDescription: "如果您想表明此帐户是一只猫,请打开此标志。\n开启后会在您的头像上出现猫耳朵并将你的帖子中的「na」替换为「nya」日文同理。"
flagShowTimelineReplies: "在时间线上显示帖子的回复" flagShowTimelineReplies: "在时间线上显示帖子的回复"
@ -989,6 +989,16 @@ enableChartsForFederatedInstances: "生成远程服务器的图表"
showClipButtonInNoteFooter: "在贴文下方显示便签按钮" showClipButtonInNoteFooter: "在贴文下方显示便签按钮"
largeNoteReactions: "使用大图标来显示回应" largeNoteReactions: "使用大图标来显示回应"
noteIdOrUrl: "帖子ID或URL" noteIdOrUrl: "帖子ID或URL"
accountMigration: "账户迁移"
accountMoved: "此用户已迁移账户"
_accountMigration:
moveTo: "把这个账户迁移到新的账户"
moveToLabel: "迁移后的账户"
moveAccountDescription: "此操作无法取消。请先确认您已在迁移后的账户上,为此账户创造了别名。创造别名后,请如以下输入您的迁移后的账户:@person@instance.com"
moveFrom: "从别的账号迁移到此账户"
moveFromLabel: "迁移前的账户"
moveFromDescription: "如果迁移时需要继承其他账户的关注者,请在此创造别名。此操作需要在实行迁移之前完成!请如已下输入需要迁移的账户:@person@instance.com"
migrationConfirm: "确定要把此账户迁移到{account}吗?一旦确定后,此操作无法取消,此账户也无法以原来的状态使用。\n同时请确认迁移后的账户已创造别名。"
_achievements: _achievements:
earnedAt: "达成时间" earnedAt: "达成时间"
_types: _types:

View file

@ -984,6 +984,10 @@ enableChartsForFederatedInstances: "生成遠端伺服器的圖表"
showClipButtonInNoteFooter: "將摘錄添加至貼文" showClipButtonInNoteFooter: "將摘錄添加至貼文"
largeNoteReactions: "將貼文的反應放大顯示" largeNoteReactions: "將貼文的反應放大顯示"
noteIdOrUrl: "貼文ID或URL" noteIdOrUrl: "貼文ID或URL"
accountMigration: "遷移帳戶"
_accountMigration:
moveTo: "將這個帳戶遷移至新的帳戶"
moveToLabel: "要遷移的帳戶:"
_achievements: _achievements:
earnedAt: "獲得日期" earnedAt: "獲得日期"
_types: _types:

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "13.11.0", "version": "13.11.1",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -44,7 +44,11 @@ export class CustomEmojiService {
memoryCacheLifetime: 1000 * 60 * 3, // 3m memoryCacheLifetime: 1000 * 60 * 3, // 3m
fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))), fetcher: () => this.emojisRepository.find({ where: { host: IsNull() } }).then(emojis => new Map(emojis.map(emoji => [emoji.name, emoji]))),
toRedisConverter: (value) => JSON.stringify(value.values()), 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型の変換
}); });
} }

View file

@ -29,6 +29,7 @@ export class FederatedInstanceService {
toRedisConverter: (value) => JSON.stringify(value), toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => { fromRedisConverter: (value) => {
const parsed = JSON.parse(value); const parsed = JSON.parse(value);
if (parsed == null) return null;
return { return {
...parsed, ...parsed,
firstRetrievedAt: new Date(parsed.firstRetrievedAt), firstRetrievedAt: new Date(parsed.firstRetrievedAt),

View file

@ -3,10 +3,11 @@ import { ulid } from 'ulid';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { genAid, parseAid } from '@/misc/id/aid.js'; import { genAid, parseAid } from '@/misc/id/aid.js';
import { genMeid } from '@/misc/id/meid.js'; import { genMeid, parseMeid } from '@/misc/id/meid.js';
import { genMeidg } from '@/misc/id/meidg.js'; import { genMeidg, parseMeidg } from '@/misc/id/meidg.js';
import { genObjectId } from '@/misc/id/object-id.js'; import { genObjectId } from '@/misc/id/object-id.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { parseUlid } from '@/misc/id/ulid.js';
@Injectable() @Injectable()
export class IdService { export class IdService {
@ -37,11 +38,10 @@ export class IdService {
public parse(id: string): { date: Date; } { public parse(id: string): { date: Date; } {
switch (this.method) { switch (this.method) {
case 'aid': return parseAid(id); case 'aid': return parseAid(id);
// TODO case 'objectid':
//case 'meid': case 'meid': return parseMeid(id);
//case 'meidg': case 'meidg': return parseMeidg(id);
//case 'ulid': case 'ulid': return parseUlid(id);
//case 'objectid':
default: throw new Error('unrecognized id generation method'); default: throw new Error('unrecognized id generation method');
} }
} }

View file

@ -186,7 +186,7 @@ class DeliverManager {
for (const following of followers) { for (const following of followers) {
const inbox = following.followerSharedInbox ?? following.followerInbox; const inbox = following.followerSharedInbox ?? following.followerInbox;
inboxes.set(inbox, following.followerSharedInbox === null); inboxes.set(inbox, following.followerSharedInbox != null);
} }
} }

View file

@ -8,7 +8,7 @@ export class RedisKVCache<T> {
private memoryCache: MemoryKVCache<T>; private memoryCache: MemoryKVCache<T>;
private fetcher: (key: string) => Promise<T>; private fetcher: (key: string) => Promise<T>;
private toRedisConverter: (value: T) => string; private toRedisConverter: (value: T) => string;
private fromRedisConverter: (value: string) => T; private fromRedisConverter: (value: string) => T | undefined; // undefined means no cache
constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: { constructor(redisClient: RedisKVCache<T>['redisClient'], name: RedisKVCache<T>['name'], opts: {
lifetime: RedisKVCache<T>['lifetime']; lifetime: RedisKVCache<T>['lifetime'];
@ -92,7 +92,7 @@ export class RedisSingleCache<T> {
private memoryCache: MemorySingleCache<T>; private memoryCache: MemorySingleCache<T>;
private fetcher: () => Promise<T>; private fetcher: () => Promise<T>;
private toRedisConverter: (value: T) => string; private toRedisConverter: (value: T) => string;
private fromRedisConverter: (value: string) => T; private fromRedisConverter: (value: string) => T | undefined; // undefined means no cache
constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: { constructor(redisClient: RedisSingleCache<T>['redisClient'], name: RedisSingleCache<T>['name'], opts: {
lifetime: RedisSingleCache<T>['lifetime']; lifetime: RedisSingleCache<T>['lifetime'];

View file

@ -3,6 +3,8 @@
import * as crypto from 'node:crypto'; import * as crypto from 'node:crypto';
export const aidRegExp = /^[0-9a-z]{10}$/;
const TIME2000 = 946684800000; const TIME2000 = 946684800000;
let counter = crypto.randomBytes(2).readUInt16LE(0); let counter = crypto.randomBytes(2).readUInt16LE(0);

View file

@ -1,5 +1,8 @@
const CHARS = '0123456789abcdef'; const CHARS = '0123456789abcdef';
// same as object-id
export const meidRegExp = /^[0-9a-f]{24}$/;
function getTime(time: number) { function getTime(time: number) {
if (time < 0) time = 0; if (time < 0) time = 0;
if (time === 0) { if (time === 0) {
@ -24,3 +27,9 @@ function getRandom() {
export function genMeid(date: Date): string { export function genMeid(date: Date): string {
return getTime(date.getTime()) + getRandom(); return getTime(date.getTime()) + getRandom();
} }
export function parseMeid(id: string): { date: Date; } {
return {
date: new Date(parseInt(id.slice(0, 12), 16) - 0x800000000000),
};
}

View file

@ -3,6 +3,7 @@ const CHARS = '0123456789abcdef';
// 4bit Fixed hex value 'g' // 4bit Fixed hex value 'g'
// 44bit UNIX Time ms in Hex // 44bit UNIX Time ms in Hex
// 48bit Random value in Hex // 48bit Random value in Hex
export const meidgRegExp = /^g[0-9a-f]{23}$/;
function getTime(time: number) { function getTime(time: number) {
if (time < 0) time = 0; if (time < 0) time = 0;
@ -26,3 +27,9 @@ function getRandom() {
export function genMeidg(date: Date): string { export function genMeidg(date: Date): string {
return 'g' + getTime(date.getTime()) + getRandom(); return 'g' + getTime(date.getTime()) + getRandom();
} }
export function parseMeidg(id: string): { date: Date; } {
return {
date: new Date(parseInt(id.slice(1, 12), 16)),
};
}

View file

@ -1,5 +1,8 @@
const CHARS = '0123456789abcdef'; const CHARS = '0123456789abcdef';
// same as meid
export const objectIdRegExp = /^[0-9a-f]{24}$/;
function getTime(time: number) { function getTime(time: number) {
if (time < 0) time = 0; if (time < 0) time = 0;
if (time === 0) { if (time === 0) {
@ -24,3 +27,9 @@ function getRandom() {
export function genObjectId(date: Date): string { export function genObjectId(date: Date): string {
return getTime(date.getTime()) + getRandom(); return getTime(date.getTime()) + getRandom();
} }
export function parseObjectId(id: string): { date: Date; } {
return {
date: new Date(parseInt(id.slice(0, 8), 16) * 1000),
};
}

View file

@ -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) };
}

View file

@ -75,13 +75,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
let timeline: Note[] = []; let timeline: Note[] = [];
const noteIdsRes = await this.redisClient.xrevrange( const limit = ps.limit + (ps.untilId ? 1 : 0); // untilIdに指定したものも含まれるため+1
`channelTimeline:${channel.id}`, let noteIdsRes: [string, string[]][] = [];
ps.untilId ? this.idService.parse(ps.untilId).date.getTime() : '+',
'-', if (!ps.sinceId && !ps.sinceDate) {
'COUNT', ps.limit + 1); // untilIdに指定したものも含まれるため+1 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 //#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate) const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.channelId = :channelId', { channelId: channel.id }) .andWhere('note.channelId = :channelId', { channelId: channel.id })

View file

@ -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());
});
});

View file

@ -439,6 +439,7 @@ defineExpose({
&.asDrawer { &.asDrawer {
width: 100% !important; width: 100% !important;
padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
> .emojis { > .emojis {
::v-deep(section) { ::v-deep(section) {

View file

@ -13,8 +13,6 @@ export class UserPreview {
this.el = el; this.el = el;
this.user = user; this.user = user;
this.attach();
this.show = this.show.bind(this); this.show = this.show.bind(this);
this.close = this.close.bind(this); this.close = this.close.bind(this);
this.onMouseover = this.onMouseover.bind(this); this.onMouseover = this.onMouseover.bind(this);
@ -22,6 +20,8 @@ export class UserPreview {
this.onClick = this.onClick.bind(this); this.onClick = this.onClick.bind(this);
this.attach = this.attach.bind(this); this.attach = this.attach.bind(this);
this.detach = this.detach.bind(this); this.detach = this.detach.bind(this);
this.attach();
} }
private show() { private show() {

View file

@ -2,7 +2,7 @@
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700"> <MkSpacer :content-max="700">
<div v-if="channel" class="_gaps_m"> <div v-if="channelId == null || channel != null" class="_gaps_m">
<MkInput v-model="name"> <MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template> <template #label>{{ i18n.ts.name }}</template>
</MkInput> </MkInput>

View file

@ -47,6 +47,7 @@ const featuredPagination = {
const favoritesPagination = { const favoritesPagination = {
endpoint: 'channels/my-favorites' as const, endpoint: 'channels/my-favorites' as const,
limit: 100, limit: 100,
noPaging: true,
}; };
const followingPagination = { const followingPagination = {
endpoint: 'channels/followed' as const, endpoint: 'channels/followed' as const,

View file

@ -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: { const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
state: 'alive', state: 'alive',
origin: 'local', origin: 'local',

View file

@ -32,6 +32,7 @@ import { clipsCache } from '@/cache';
const pagination = { const pagination = {
endpoint: 'clips/list' as const, endpoint: 'clips/list' as const,
noPaging: true,
limit: 10, limit: 10,
}; };

View file

@ -30,6 +30,7 @@ const pagingComponent = $shallowRef<InstanceType<typeof MkPagination>>();
const pagination = { const pagination = {
endpoint: 'users/lists/list' as const, endpoint: 'users/lists/list' as const,
noPaging: true,
limit: 10, limit: 10,
}; };