Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8682d4a5f | |||
| 3baaa94f0e | |||
| e38d398de0 | |||
| b04ba05063 | |||
| b5cb5b65e6 | |||
| 9ca9feb518 | |||
| 7a2e2a5d5f | |||
| 39bd62f1a8 | |||
| 2048b3f76e | |||
| ba15e24519 | |||
| 402ed23ca4 | |||
| a7b876670d | |||
| 0b6ace990e | |||
| 2a1dbd2756 | |||
| a9feba4efd | |||
| 5cfe5d4f58 | |||
| c90443b37a |
@@ -777,11 +777,11 @@ msgstr "Canvia el teu correu"
|
||||
|
||||
#: src/Navigation.tsx:302
|
||||
msgid "Chat"
|
||||
msgstr ""
|
||||
msgstr "Xat"
|
||||
|
||||
#: src/screens/Messages/Conversation/index.tsx:26
|
||||
msgid "Chat with {chatId}"
|
||||
msgstr ""
|
||||
msgstr "Xateja amb {chatId}"
|
||||
|
||||
#: src/screens/Deactivated.tsx:79
|
||||
#: src/screens/Deactivated.tsx:83
|
||||
@@ -2858,18 +2858,18 @@ msgstr "Missatge del servidor: {0}"
|
||||
|
||||
#: src/screens/Messages/List/index.tsx:34
|
||||
msgid "Message settings"
|
||||
msgstr ""
|
||||
msgstr "Configuració dels missatges"
|
||||
|
||||
#: src/Navigation.tsx:517
|
||||
#: src/screens/Messages/List/index.tsx:103
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:261
|
||||
#: src/view/shell/desktop/LeftNav.tsx:360
|
||||
msgid "Messages"
|
||||
msgstr ""
|
||||
msgstr "Missatges"
|
||||
|
||||
#: src/Navigation.tsx:307
|
||||
msgid "Messaging settings"
|
||||
msgstr ""
|
||||
msgstr "Configuració dels missatges"
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:45
|
||||
msgid "Misleading Account"
|
||||
@@ -3569,7 +3569,7 @@ msgstr "Obre la web enllaçada"
|
||||
|
||||
#: src/screens/Messages/List/index.tsx:35
|
||||
msgid "Opens the message settings page"
|
||||
msgstr ""
|
||||
msgstr "Obre la pàgina de configuració dels missatges"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:788
|
||||
#: src/view/screens/Settings/index.tsx:798
|
||||
@@ -4044,7 +4044,7 @@ msgstr "Elimina la paraula silenciada de la teva llista"
|
||||
|
||||
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:208
|
||||
msgid "Remove quote"
|
||||
msgstr ""
|
||||
msgstr "Elimina la citació"
|
||||
|
||||
#: src/view/com/modals/Repost.tsx:48
|
||||
msgid "Remove repost"
|
||||
@@ -4081,7 +4081,7 @@ msgstr "Elimina la miniatura per defecte de {0}"
|
||||
|
||||
#: src/view/com/util/post-embeds/QuoteEmbed.tsx:209
|
||||
msgid "Removes quoted post"
|
||||
msgstr ""
|
||||
msgstr "Elimina la publicació amb la citació"
|
||||
|
||||
#: src/view/screens/Profile.tsx:194
|
||||
msgid "Replies"
|
||||
@@ -4413,7 +4413,7 @@ msgstr "Cerca per \"{query}\""
|
||||
|
||||
#: src/view/screens/Search/Search.tsx:839
|
||||
msgid "Search for \"{searchText}\""
|
||||
msgstr ""
|
||||
msgstr "Cerca per \"{searchText}\""
|
||||
|
||||
#: src/components/TagMenu/index.tsx:145
|
||||
msgid "Search for all posts by @{authorHandle} with tag {displayTag}"
|
||||
@@ -6261,7 +6261,7 @@ msgstr "No tens llistes."
|
||||
|
||||
#: src/screens/Messages/List/index.tsx:92
|
||||
msgid "You have no messages yet. Start a conversation with someone!"
|
||||
msgstr ""
|
||||
msgstr "Encara no tens missatges. Comença una conversa amb algú!"
|
||||
|
||||
#: src/view/screens/ModerationBlockedAccounts.tsx:137
|
||||
msgid "You have not blocked any accounts yet. To block an account, go to their profile and select \"Block account\" from the menu on their account."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Language: ja\n"
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"PO-Revision-Date: 2024-04-27 16:07+0900\n"
|
||||
"PO-Revision-Date: 2024-05-06 15:47+0900\n"
|
||||
"Last-Translator: tkusano\n"
|
||||
"Language-Team: Hima-Zinn, tkusano, dolciss, oboenikui, noritada, middlingphys, hibiki, reindex-ot, haoyayoi, vyv03354\n"
|
||||
"Plural-Forms: \n"
|
||||
@@ -247,6 +247,10 @@ msgstr "以前のメールアドレス{0}にメールが送信されました。
|
||||
msgid "An error occured"
|
||||
msgstr "エラーが発生しました"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:132
|
||||
msgid "An error occurred while trying to delete the message. Please try again."
|
||||
msgstr "メッセージ削除中にエラーが発生しました。再実行してみてください。"
|
||||
|
||||
#: src/lib/moderation/useReportOptions.ts:26
|
||||
msgid "An issue not included in these options"
|
||||
msgstr "ほかの選択肢にはあてはまらない問題"
|
||||
@@ -324,6 +328,14 @@ msgstr "背景"
|
||||
msgid "Are you sure you want to delete the app password \"{name}\"?"
|
||||
msgstr "アプリパスワード「{name}」を本当に削除しますか?"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:90
|
||||
msgid "Are you sure you want to delete this message? The message will be deleted for you, but not for other participants."
|
||||
msgstr "このメッセージを本当に削除しますか?このメッセージはあなたからは削除したように見えますが、他の参加者からは削除されません。"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:166
|
||||
msgid "Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for other participants."
|
||||
msgstr "この会話から退出しますか?あなたのメッセージはあなたからは削除したように見えますが、他の参加者からは削除されません。"
|
||||
|
||||
#: src/view/com/feeds/FeedSourceCard.tsx:280
|
||||
msgid "Are you sure you want to remove {0} from your feeds?"
|
||||
msgstr "あなたのフィードから{0}を削除してもよろしいですか?"
|
||||
@@ -389,6 +401,11 @@ msgstr "生年月日:"
|
||||
msgid "Block"
|
||||
msgstr "ブロック"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:129
|
||||
#: src/components/dms/ConvoMenu.tsx:133
|
||||
msgid "Block account"
|
||||
msgstr "アカウントをブロック"
|
||||
|
||||
#: src/view/com/profile/ProfileMenu.tsx:300
|
||||
#: src/view/com/profile/ProfileMenu.tsx:307
|
||||
msgid "Block Account"
|
||||
@@ -617,9 +634,17 @@ msgstr "メールアドレスを変更"
|
||||
msgid "Chat"
|
||||
msgstr "チャット"
|
||||
|
||||
#: src/screens/Messages/Conversation/index.tsx:26
|
||||
msgid "Chat with {chatId}"
|
||||
msgstr "{chatId}とのチャット"
|
||||
#: src/components/dms/ConvoMenu.tsx:55
|
||||
msgid "Chat muted"
|
||||
msgstr "チャットをミュートしました"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:87
|
||||
msgid "Chat settings"
|
||||
msgstr "チャットの設定"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:65
|
||||
msgid "Chat unmuted"
|
||||
msgstr "チャットのミュートを解除しました"
|
||||
|
||||
#: src/screens/Deactivated.tsx:79
|
||||
#: src/screens/Deactivated.tsx:83
|
||||
@@ -966,6 +991,11 @@ msgstr "リストへのリンクをコピー"
|
||||
msgid "Copy link to post"
|
||||
msgstr "投稿へのリンクをコピー"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:89
|
||||
#: src/components/dms/MessageMenu.tsx:91
|
||||
msgid "Copy message text"
|
||||
msgstr "メッセージのテキストをコピー"
|
||||
|
||||
#: src/view/com/util/forms/PostDropdownBtn.tsx:230
|
||||
#: src/view/com/util/forms/PostDropdownBtn.tsx:232
|
||||
msgid "Copy post text"
|
||||
@@ -976,6 +1006,10 @@ msgstr "投稿のテキストをコピー"
|
||||
msgid "Copyright Policy"
|
||||
msgstr "著作権ポリシー"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:79
|
||||
msgid "Could not leave chat"
|
||||
msgstr "チャットからの退出に失敗しました"
|
||||
|
||||
#: src/view/screens/ProfileFeed.tsx:103
|
||||
msgid "Could not load feed"
|
||||
msgstr "フィードの読み込みに失敗しました"
|
||||
@@ -984,6 +1018,18 @@ msgstr "フィードの読み込みに失敗しました"
|
||||
msgid "Could not load list"
|
||||
msgstr "リストの読み込みに失敗しました"
|
||||
|
||||
#: src/components/dms/NewChat.tsx:225
|
||||
msgid "Could not load profiles. Please try again later."
|
||||
msgstr "プロフィールの読み込みに失敗しました。時間をおいてもう一度お試しください。"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:58
|
||||
msgid "Could not mute chat"
|
||||
msgstr "チャットのミュートに失敗しました"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:68
|
||||
msgid "Could not unmute chat"
|
||||
msgstr "チャットのミュートの解除に失敗しました"
|
||||
|
||||
#: src/view/com/auth/SplashScreen.tsx:57
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:101
|
||||
msgid "Create a new account"
|
||||
@@ -1088,10 +1134,23 @@ msgstr "アプリパスワードを削除"
|
||||
msgid "Delete app password?"
|
||||
msgstr "アプリパスワードを削除しますか?"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:101
|
||||
msgid "Delete for me"
|
||||
msgstr "自分宛を削除"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:417
|
||||
msgid "Delete List"
|
||||
msgstr "リストを削除"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:68
|
||||
#: src/components/dms/MessageMenu.tsx:88
|
||||
msgid "Delete message"
|
||||
msgstr "メッセージを削除"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:99
|
||||
msgid "Delete message for me"
|
||||
msgstr "メッセージの宛先から自分を削除"
|
||||
|
||||
#: src/view/com/modals/DeleteAccount.tsx:223
|
||||
msgid "Delete my account"
|
||||
msgstr "マイアカウントを削除"
|
||||
@@ -1584,6 +1643,10 @@ msgstr "アプリパスワードの作成に失敗しました。"
|
||||
msgid "Failed to create the list. Check your internet connection and try again."
|
||||
msgstr "リストの作成に失敗しました。インターネットへの接続を確認の上、もう一度お試しください。"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:130
|
||||
msgid "Failed to delete message"
|
||||
msgstr "メッセージの削除に失敗しました"
|
||||
|
||||
#: src/view/com/util/forms/PostDropdownBtn.tsx:131
|
||||
msgid "Failed to delete post, please try again"
|
||||
msgstr "投稿の削除に失敗しました。もう一度お試しください。"
|
||||
@@ -1592,6 +1655,10 @@ msgstr "投稿の削除に失敗しました。もう一度お試しください
|
||||
msgid "Failed to load GIFs"
|
||||
msgstr "GIFの読み込みに失敗しました"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessageListError.tsx:21
|
||||
msgid "Failed to load past messages."
|
||||
msgstr "過去のメッセージの読み込みに失敗しました。"
|
||||
|
||||
#: src/view/com/lightbox/Lightbox.tsx:83
|
||||
msgid "Failed to save image: {0}"
|
||||
msgstr "画像の保存に失敗しました:{0}"
|
||||
@@ -1853,16 +1920,19 @@ msgstr "ホームへ"
|
||||
msgid "Go Home"
|
||||
msgstr "ホームへ"
|
||||
|
||||
#: src/view/screens/Search/Search.tsx:827
|
||||
#: src/view/shell/desktop/Search.tsx:263
|
||||
#~ msgid "Go to @{queryMaybeHandle}"
|
||||
#~ msgstr "@{queryMaybeHandle}へ"
|
||||
|
||||
#: src/screens/Login/ForgotPasswordForm.tsx:172
|
||||
#: src/view/com/modals/ChangePassword.tsx:169
|
||||
msgid "Go to next"
|
||||
msgstr "次へ"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:109
|
||||
msgid "Go to profile"
|
||||
msgstr "プロフィールへ"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:106
|
||||
msgid "Go to user's profile"
|
||||
msgstr "ユーザーのプロフィールへ移動"
|
||||
|
||||
#: src/lib/moderation/useGlobalLabelStrings.ts:46
|
||||
msgid "Graphic Media"
|
||||
msgstr "生々しいメディア"
|
||||
@@ -2218,6 +2288,16 @@ msgstr "Blueskyで公開されている内容はこちらを参照してくだ
|
||||
msgid "Learn more."
|
||||
msgstr "詳細。"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:168
|
||||
msgid "Leave"
|
||||
msgstr "退出"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:151
|
||||
#: src/components/dms/ConvoMenu.tsx:154
|
||||
#: src/components/dms/ConvoMenu.tsx:164
|
||||
msgid "Leave conversation"
|
||||
msgstr "会話を退出"
|
||||
|
||||
#: src/view/com/modals/lang-settings/ContentLanguagesSettings.tsx:82
|
||||
msgid "Leave them all unchecked to see any language."
|
||||
msgstr "どの言語も表示するには、すべてのチェックを外したままにします。"
|
||||
@@ -2411,10 +2491,18 @@ msgstr "メンションされたユーザー"
|
||||
msgid "Menu"
|
||||
msgstr "メニュー"
|
||||
|
||||
#: src/screens/Messages/List/index.tsx:209
|
||||
msgid "Message deleted"
|
||||
msgstr "メッセージは削除されました"
|
||||
|
||||
#: src/view/com/posts/FeedErrorMessage.tsx:192
|
||||
msgid "Message from server: {0}"
|
||||
msgstr "サーバーからのメッセージ:{0}"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessageInput.tsx:74
|
||||
msgid "Message input field"
|
||||
msgstr "メッセージを入力するフィールド"
|
||||
|
||||
#: src/screens/Messages/List/index.tsx:34
|
||||
msgid "Message settings"
|
||||
msgstr "メッセージの設定"
|
||||
@@ -2543,6 +2631,11 @@ msgstr "テキストとタグをミュート"
|
||||
msgid "Mute list"
|
||||
msgstr "リストをミュート"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:114
|
||||
#: src/components/dms/ConvoMenu.tsx:120
|
||||
msgid "Mute notifications"
|
||||
msgstr "通知をミュート"
|
||||
|
||||
#: src/view/screens/ProfileList.tsx:621
|
||||
msgid "Mute these accounts?"
|
||||
msgstr "これらのアカウントをミュートしますか?"
|
||||
@@ -2665,6 +2758,12 @@ msgstr "新規"
|
||||
msgid "New"
|
||||
msgstr "新規"
|
||||
|
||||
#: src/components/dms/NewChat.tsx:59
|
||||
#: src/screens/Messages/List/index.tsx:346
|
||||
#: src/screens/Messages/List/index.tsx:354
|
||||
msgid "New chat"
|
||||
msgstr "新しいチャット"
|
||||
|
||||
#: src/view/com/modals/CreateOrEditList.tsx:255
|
||||
msgid "New Moderation List"
|
||||
msgstr "新しいモデレーションリスト"
|
||||
@@ -2755,6 +2854,11 @@ msgstr "{0}のフォローを解除しました"
|
||||
msgid "No longer than 253 characters"
|
||||
msgstr "253文字まで"
|
||||
|
||||
#: src/screens/Messages/List/index.tsx:138
|
||||
#: src/screens/Messages/List/index.tsx:198
|
||||
msgid "No messages yet"
|
||||
msgstr "メッセージはありません"
|
||||
|
||||
#: src/view/com/notifications/Feed.tsx:109
|
||||
msgid "No notifications yet!"
|
||||
msgstr "お知らせはありません!"
|
||||
@@ -2780,7 +2884,11 @@ msgstr "「{query}」の検索結果はありません"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:204
|
||||
msgid "No search results found for \"{search}\"."
|
||||
msgstr "「{search}」の検索結果はありません"
|
||||
msgstr "「{search}」の検索結果はありません。"
|
||||
|
||||
#: src/components/dms/NewChat.tsx:224
|
||||
msgid "No search results found for \"{searchText}\"."
|
||||
msgstr "「{searchText}」の検索結果はありません。"
|
||||
|
||||
#: src/components/dialogs/EmbedConsent.tsx:105
|
||||
#: src/components/dialogs/EmbedConsent.tsx:112
|
||||
@@ -2834,6 +2942,10 @@ msgstr "注記:Blueskyはオープンでパブリックなネットワーク
|
||||
msgid "Notifications"
|
||||
msgstr "通知"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessageItem.tsx:116
|
||||
msgid "Now"
|
||||
msgstr "今"
|
||||
|
||||
#: src/view/com/modals/SelfLabel.tsx:103
|
||||
msgid "Nudity"
|
||||
msgstr "ヌード"
|
||||
@@ -3218,6 +3330,10 @@ msgstr "パスワードも入力してください:"
|
||||
msgid "Please explain why you think this label was incorrectly applied by {0}"
|
||||
msgstr "{0}によって貼られたこのラベルが誤って適用されたと思われる理由を説明してください"
|
||||
|
||||
#: src/lib/hooks/useAccountSwitcher.ts:43
|
||||
msgid "Please sign in as @{0}"
|
||||
msgstr "@{0}としてサインインしてください"
|
||||
|
||||
#: src/view/com/modals/VerifyEmail.tsx:110
|
||||
msgid "Please Verify Your Email"
|
||||
msgstr "メールアドレスを確認してください"
|
||||
@@ -3316,6 +3432,11 @@ msgstr "ホスティングプロバイダーを変える"
|
||||
msgid "Press to retry"
|
||||
msgstr "再実行する"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessagesList.tsx:41
|
||||
#: src/screens/Messages/Conversation/MessagesList.tsx:47
|
||||
msgid "Press to Retry"
|
||||
msgstr "再実行"
|
||||
|
||||
#: src/view/com/lightbox/Lightbox.web.tsx:150
|
||||
msgid "Previous image"
|
||||
msgstr "前の画像"
|
||||
@@ -3520,6 +3641,15 @@ msgctxt "description"
|
||||
msgid "Reply to <0><1/></0>"
|
||||
msgstr "<0><1/></0>に返信"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:78
|
||||
msgid "Report"
|
||||
msgstr "報告"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:140
|
||||
#: src/components/dms/ConvoMenu.tsx:144
|
||||
msgid "Report account"
|
||||
msgstr "アカウントを報告"
|
||||
|
||||
#: src/view/com/profile/ProfileMenu.tsx:319
|
||||
#: src/view/com/profile/ProfileMenu.tsx:322
|
||||
msgid "Report Account"
|
||||
@@ -3538,6 +3668,10 @@ msgstr "フィードを報告"
|
||||
msgid "Report List"
|
||||
msgstr "リストを報告"
|
||||
|
||||
#: src/components/dms/MessageMenu.tsx:76
|
||||
msgid "Report message"
|
||||
msgstr "メッセージを報告"
|
||||
|
||||
#: src/view/com/util/forms/PostDropdownBtn.tsx:316
|
||||
#: src/view/com/util/forms/PostDropdownBtn.tsx:318
|
||||
msgid "Report post"
|
||||
@@ -3678,6 +3812,10 @@ msgstr "エラーになった最後のアクションをやり直す"
|
||||
msgid "Retry"
|
||||
msgstr "再試行"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessageListError.tsx:48
|
||||
msgid "Retry."
|
||||
msgstr "再試行。"
|
||||
|
||||
#: src/components/Error.tsx:95
|
||||
#: src/view/screens/ProfileList.tsx:919
|
||||
msgid "Return to previous page"
|
||||
@@ -3795,6 +3933,10 @@ msgstr "{displayTag}のすべての投稿を検索(@{authorHandle}のみ)"
|
||||
msgid "Search for all posts with tag {displayTag}"
|
||||
msgstr "{displayTag}のすべての投稿を検索(すべてのユーザー)"
|
||||
|
||||
#: src/components/dms/NewChat.tsx:226
|
||||
msgid "Search for someone to start a conversation with."
|
||||
msgstr "会話を始める相手を検索。"
|
||||
|
||||
#: src/view/com/auth/LoggedOut.tsx:105
|
||||
#: src/view/com/auth/LoggedOut.tsx:106
|
||||
#: src/view/com/modals/ListAddRemoveUsers.tsx:70
|
||||
@@ -3805,6 +3947,10 @@ msgstr "ユーザーを検索"
|
||||
msgid "Search GIFs"
|
||||
msgstr "GIFを検索"
|
||||
|
||||
#: src/components/dms/NewChat.tsx:182
|
||||
msgid "Search profiles"
|
||||
msgstr "プロフィールを検索"
|
||||
|
||||
#: src/components/dialogs/GifSelect.tsx:159
|
||||
msgid "Search Tenor"
|
||||
msgstr "Tenorを検索"
|
||||
@@ -3937,6 +4083,10 @@ msgstr "メールを送信"
|
||||
msgid "Send feedback"
|
||||
msgstr "フィードバックを送信"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessageInput.tsx:94
|
||||
msgid "Send message"
|
||||
msgstr "メッセージを送信"
|
||||
|
||||
#: src/components/ReportDialog/SubmitView.tsx:215
|
||||
#: src/components/ReportDialog/SubmitView.tsx:219
|
||||
msgid "Send report"
|
||||
@@ -4309,8 +4459,12 @@ msgstr "スポーツ"
|
||||
msgid "Square"
|
||||
msgstr "正方形"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:862
|
||||
msgid "Status page"
|
||||
#: src/components/dms/NewChat.tsx:177
|
||||
msgid "Start a new chat"
|
||||
msgstr "新しいチャットを開始"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:896
|
||||
msgid "Status Page"
|
||||
msgstr "ステータスページ"
|
||||
|
||||
#: src/screens/Signup/index.tsx:145
|
||||
@@ -4799,6 +4953,10 @@ msgstr "再試行"
|
||||
msgid "Two-factor authentication"
|
||||
msgstr "2要素認証"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessageInput.tsx:75
|
||||
msgid "Type your message here"
|
||||
msgstr "ここにメッセージを入力する"
|
||||
|
||||
#: src/view/com/modals/ChangeHandle.tsx:429
|
||||
msgid "Type:"
|
||||
msgstr "タイプ:"
|
||||
@@ -4893,6 +5051,10 @@ msgstr "アカウントのミュートを解除"
|
||||
msgid "Unmute all {displayTag} posts"
|
||||
msgstr "{displayTag}のすべての投稿のミュートを解除"
|
||||
|
||||
#: src/components/dms/ConvoMenu.tsx:118
|
||||
msgid "Unmute notifications"
|
||||
msgstr "通知のミュートを解除"
|
||||
|
||||
#: src/view/com/util/forms/PostDropdownBtn.tsx:273
|
||||
#: src/view/com/util/forms/PostDropdownBtn.tsx:278
|
||||
msgid "Unmute thread"
|
||||
@@ -5093,9 +5255,9 @@ msgstr "新しいメールアドレスを確認"
|
||||
msgid "Verify Your Email"
|
||||
msgstr "メールアドレスを確認"
|
||||
|
||||
#: src/view/screens/Settings/index.tsx:852
|
||||
msgid "Version {0}"
|
||||
msgstr "バージョン {0}"
|
||||
#: src/view/screens/Settings/index.tsx:868
|
||||
msgid "Version {appVersion} {bundleInfo}"
|
||||
msgstr "バージョン {appVersion} {bundleInfo}"
|
||||
|
||||
#: src/screens/Onboarding/index.tsx:42
|
||||
msgid "Video Games"
|
||||
@@ -5279,6 +5441,11 @@ msgstr "なぜこのユーザーをレビューする必要がありますか?
|
||||
msgid "Wide"
|
||||
msgstr "ワイド"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessageInput.tsx:76
|
||||
#: src/screens/Messages/Conversation/MessageInput.web.tsx:71
|
||||
msgid "Write a message"
|
||||
msgstr "メッセージを書く"
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:468
|
||||
msgid "Write post"
|
||||
msgstr "投稿を書く"
|
||||
@@ -5302,6 +5469,10 @@ msgstr "ライター"
|
||||
msgid "Yes"
|
||||
msgstr "はい"
|
||||
|
||||
#: src/screens/Messages/Conversation/MessageItem.tsx:130
|
||||
msgid "Yesterday, {time}"
|
||||
msgstr "昨日、{time}"
|
||||
|
||||
#: src/screens/Deactivated.tsx:137
|
||||
msgid "You are in line."
|
||||
msgstr "あなたは並んでいます。"
|
||||
@@ -5435,6 +5606,10 @@ msgstr "これ以降、このスレッドに関する通知を受け取ること
|
||||
msgid "You will receive an email with a \"reset code.\" Enter that code here, then enter your new password."
|
||||
msgstr "「リセットコード」が記載されたメールが届きます。ここにコードを入力し、新しいパスワードを入力します。"
|
||||
|
||||
#: src/screens/Messages/List/index.tsx:202
|
||||
msgid "You: {0}"
|
||||
msgstr "あなた: {0}"
|
||||
|
||||
#: src/screens/Onboarding/StepModeration/index.tsx:60
|
||||
msgid "You're in control"
|
||||
msgstr "あなたがコントロールしています"
|
||||
|
||||
+434
-244
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ConvoError, ConvoItem} from '#/state/messages/convo'
|
||||
import {ConvoItem, ConvoItemError} from '#/state/messages/convo'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
import {InlineLinkText} from '#/components/Link'
|
||||
@@ -18,7 +18,13 @@ export function MessageListError({
|
||||
const {_} = useLingui()
|
||||
const message = React.useMemo(() => {
|
||||
return {
|
||||
[ConvoError.HistoryFailed]: _(msg`Failed to load past messages.`),
|
||||
[ConvoItemError.Network]: _(
|
||||
msg`There was an issue connecting to the chat.`,
|
||||
),
|
||||
[ConvoItemError.HistoryFailed]: _(msg`Failed to load past messages.`),
|
||||
[ConvoItemError.PollFailed]: _(
|
||||
msg`This chat was disconnected due to a network error.`,
|
||||
),
|
||||
}[item.code]
|
||||
}, [_, item.code])
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ export function MessagesList() {
|
||||
<ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
|
||||
<List
|
||||
ref={flatListRef}
|
||||
data={chat.status === ConvoStatus.Ready ? chat.items : undefined}
|
||||
data={chat.items}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
disableVirtualization={true}
|
||||
@@ -248,11 +248,7 @@ export function MessagesList() {
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
scrollEventThrottle={100}
|
||||
ListHeaderComponent={
|
||||
<MaybeLoader
|
||||
isLoading={
|
||||
chat.status === ConvoStatus.Ready && chat.isFetchingHistory
|
||||
}
|
||||
/>
|
||||
<MaybeLoader isLoading={chat.isFetchingHistory} />
|
||||
}
|
||||
/>
|
||||
</ScrollProvider>
|
||||
|
||||
@@ -14,11 +14,11 @@ import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {ChatProvider, useChat} from 'state/messages'
|
||||
import {ConvoStatus} from 'state/messages/convo'
|
||||
import {useSession} from 'state/session'
|
||||
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||
import {ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -43,28 +43,40 @@ export function MessagesConversationScreen({route}: Props) {
|
||||
|
||||
function Inner() {
|
||||
const chat = useChat()
|
||||
const {currentAccount} = useSession()
|
||||
const myDid = currentAccount?.did
|
||||
|
||||
const otherProfile = React.useMemo(() => {
|
||||
if (chat.status !== ConvoStatus.Ready) return
|
||||
return chat.convo.members.find(m => m.did !== myDid)
|
||||
}, [chat, myDid])
|
||||
if (
|
||||
chat.status === ConvoStatus.Uninitialized ||
|
||||
chat.status === ConvoStatus.Initializing
|
||||
) {
|
||||
return <ListMaybePlaceholder isLoading />
|
||||
}
|
||||
|
||||
// TODO whenever we have error messages, we should use them in here -hailey
|
||||
if (chat.status !== ConvoStatus.Ready || !otherProfile) {
|
||||
if (chat.status === ConvoStatus.Error) {
|
||||
// TODO
|
||||
return (
|
||||
<ListMaybePlaceholder
|
||||
isLoading={true}
|
||||
isError={chat.status === ConvoStatus.Error}
|
||||
/>
|
||||
<View>
|
||||
<CenteredView style={{flex: 1}} sideBorders>
|
||||
<Text>Something went wrong</Text>
|
||||
<Button
|
||||
label="Retry"
|
||||
onPress={() => {
|
||||
chat.error.retry()
|
||||
}}>
|
||||
<ButtonText>Retry</ButtonText>
|
||||
</Button>
|
||||
</CenteredView>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Any other chat states (atm) are "ready" states
|
||||
*/
|
||||
|
||||
return (
|
||||
<KeyboardProvider>
|
||||
<CenteredView style={{flex: 1}} sideBorders>
|
||||
<Header profile={otherProfile} />
|
||||
<Header profile={chat.recipients[0]} />
|
||||
<MessagesList />
|
||||
</CenteredView>
|
||||
</KeyboardProvider>
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import {describe, it} from '@jest/globals'
|
||||
|
||||
describe(`#/state/messages/convo`, () => {
|
||||
describe(`status states`, () => {
|
||||
describe(`init`, () => {
|
||||
it.todo(`fails if sender and recipients aren't found`)
|
||||
it.todo(`cannot re-initialize from a non-unintialized state`)
|
||||
it.todo(`can re-initialize from a failed state`)
|
||||
})
|
||||
|
||||
describe(`destroy`, () => {
|
||||
it.todo(`cannot be interacted with when destroyed`)
|
||||
it.todo(`polling is stopped when destroyed`)
|
||||
it.todo(`events are cleaned up when destroyed`)
|
||||
})
|
||||
describe(`resume`, () => {
|
||||
it.todo(`restores previous state if resume fails`)
|
||||
})
|
||||
|
||||
describe(`suspend`, () => {
|
||||
it.todo(`cannot be interacted with when suspended`)
|
||||
it.todo(`polling is stopped when suspended`)
|
||||
})
|
||||
|
||||
describe(`read states`, () => {
|
||||
|
||||
+480
-142
@@ -2,6 +2,7 @@ import {AppBskyActorDefs} from '@atproto/api'
|
||||
import {
|
||||
BskyAgent,
|
||||
ChatBskyConvoDefs,
|
||||
ChatBskyConvoGetLog,
|
||||
ChatBskyConvoSendMessage,
|
||||
} from '@atproto-labs/api'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
@@ -18,17 +19,58 @@ export type ConvoParams = {
|
||||
export enum ConvoStatus {
|
||||
Uninitialized = 'uninitialized',
|
||||
Initializing = 'initializing',
|
||||
Resuming = 'resuming',
|
||||
Ready = 'ready',
|
||||
Error = 'error',
|
||||
Backgrounded = 'backgrounded',
|
||||
Suspended = 'suspended',
|
||||
}
|
||||
|
||||
export enum ConvoError {
|
||||
export enum ConvoItemError {
|
||||
HistoryFailed = 'historyFailed',
|
||||
PollFailed = 'pollFailed',
|
||||
Network = 'network',
|
||||
}
|
||||
|
||||
export enum ConvoErrorCode {
|
||||
InitFailed = 'initFailed',
|
||||
}
|
||||
|
||||
export type ConvoError = {
|
||||
code: ConvoErrorCode
|
||||
exception?: Error
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
export enum ConvoDispatchEvent {
|
||||
Init = 'init',
|
||||
Ready = 'ready',
|
||||
Resume = 'resume',
|
||||
Background = 'background',
|
||||
Suspend = 'suspend',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export type ConvoDispatch =
|
||||
| {
|
||||
event: ConvoDispatchEvent.Init
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Ready
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Resume
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Background
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Suspend
|
||||
}
|
||||
| {
|
||||
event: ConvoDispatchEvent.Error
|
||||
payload: ConvoError
|
||||
}
|
||||
|
||||
export type ConvoItem =
|
||||
| {
|
||||
type: 'message' | 'pending-message'
|
||||
@@ -56,14 +98,9 @@ export type ConvoItem =
|
||||
| {
|
||||
type: 'error-recoverable'
|
||||
key: string
|
||||
code: ConvoError
|
||||
code: ConvoItemError
|
||||
retry: () => void
|
||||
}
|
||||
| {
|
||||
type: 'error-fatal'
|
||||
code: ConvoError
|
||||
key: string
|
||||
}
|
||||
|
||||
export type ConvoState =
|
||||
| {
|
||||
@@ -71,6 +108,8 @@ export type ConvoState =
|
||||
items: []
|
||||
convo: undefined
|
||||
error: undefined
|
||||
sender: undefined
|
||||
recipients: undefined
|
||||
isFetchingHistory: false
|
||||
deleteMessage: undefined
|
||||
sendMessage: undefined
|
||||
@@ -81,6 +120,8 @@ export type ConvoState =
|
||||
items: []
|
||||
convo: undefined
|
||||
error: undefined
|
||||
sender: undefined
|
||||
recipients: undefined
|
||||
isFetchingHistory: boolean
|
||||
deleteMessage: undefined
|
||||
sendMessage: undefined
|
||||
@@ -91,6 +132,8 @@ export type ConvoState =
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
error: undefined
|
||||
sender: AppBskyActorDefs.ProfileViewBasic
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[]
|
||||
isFetchingHistory: boolean
|
||||
deleteMessage: (messageId: string) => Promise<void>
|
||||
sendMessage: (
|
||||
@@ -103,6 +146,8 @@ export type ConvoState =
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
error: undefined
|
||||
sender: AppBskyActorDefs.ProfileViewBasic
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[]
|
||||
isFetchingHistory: boolean
|
||||
deleteMessage: (messageId: string) => Promise<void>
|
||||
sendMessage: (
|
||||
@@ -115,18 +160,8 @@ export type ConvoState =
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
error: undefined
|
||||
isFetchingHistory: boolean
|
||||
deleteMessage: (messageId: string) => Promise<void>
|
||||
sendMessage: (
|
||||
message: ChatBskyConvoSendMessage.InputSchema['message'],
|
||||
) => Promise<void>
|
||||
fetchMessageHistory: () => Promise<void>
|
||||
}
|
||||
| {
|
||||
status: ConvoStatus.Resuming
|
||||
items: ConvoItem[]
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
error: undefined
|
||||
sender: AppBskyActorDefs.ProfileViewBasic
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[]
|
||||
isFetchingHistory: boolean
|
||||
deleteMessage: (messageId: string) => Promise<void>
|
||||
sendMessage: (
|
||||
@@ -139,15 +174,20 @@ export type ConvoState =
|
||||
items: []
|
||||
convo: undefined
|
||||
error: any
|
||||
sender: undefined
|
||||
recipients: undefined
|
||||
isFetchingHistory: false
|
||||
deleteMessage: undefined
|
||||
sendMessage: undefined
|
||||
fetchMessageHistory: undefined
|
||||
}
|
||||
|
||||
const ACTIVE_POLL_INTERVAL = 2e3
|
||||
const ACTIVE_POLL_INTERVAL = 1e3
|
||||
const BACKGROUND_POLL_INTERVAL = 10e3
|
||||
|
||||
// TODO temporary
|
||||
let DEBUG_ACTIVE_CHAT: string | undefined
|
||||
|
||||
export function isConvoItemMessage(
|
||||
item: ConvoItem,
|
||||
): item is ConvoItem & {type: 'message'} {
|
||||
@@ -160,12 +200,20 @@ export function isConvoItemMessage(
|
||||
}
|
||||
|
||||
export class Convo {
|
||||
private id: string
|
||||
|
||||
private agent: BskyAgent
|
||||
private __tempFromUserDid: string
|
||||
|
||||
private pollInterval = ACTIVE_POLL_INTERVAL
|
||||
private status: ConvoStatus = ConvoStatus.Uninitialized
|
||||
private error: any
|
||||
private pollInterval = ACTIVE_POLL_INTERVAL
|
||||
private error:
|
||||
| {
|
||||
code: ConvoErrorCode
|
||||
exception?: Error
|
||||
retry: () => void
|
||||
}
|
||||
| undefined
|
||||
private historyCursor: string | undefined | null = undefined
|
||||
private isFetchingHistory = false
|
||||
private eventsCursor: string | undefined = undefined
|
||||
@@ -186,15 +234,18 @@ export class Convo {
|
||||
private footerItems: Map<string, ConvoItem> = new Map()
|
||||
private headerItems: Map<string, ConvoItem> = new Map()
|
||||
|
||||
private pendingEventIngestion: Promise<void> | undefined
|
||||
private isProcessingPendingMessages = false
|
||||
private pendingPoll: Promise<void> | undefined
|
||||
private nextPoll: NodeJS.Timeout | undefined
|
||||
|
||||
convoId: string
|
||||
convo: ChatBskyConvoDefs.ConvoView | undefined
|
||||
sender: AppBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[] | undefined = undefined
|
||||
snapshot: ConvoState | undefined
|
||||
|
||||
constructor(params: ConvoParams) {
|
||||
this.id = nanoid(3)
|
||||
this.convoId = params.convoId
|
||||
this.agent = params.agent
|
||||
this.__tempFromUserDid = params.__tempFromUserDid
|
||||
@@ -204,6 +255,14 @@ export class Convo {
|
||||
this.sendMessage = this.sendMessage.bind(this)
|
||||
this.deleteMessage = this.deleteMessage.bind(this)
|
||||
this.fetchMessageHistory = this.fetchMessageHistory.bind(this)
|
||||
|
||||
if (DEBUG_ACTIVE_CHAT) {
|
||||
logger.error(`Convo: another chat was already active`, {
|
||||
convoId: this.convoId,
|
||||
})
|
||||
} else {
|
||||
DEBUG_ACTIVE_CHAT = this.convoId
|
||||
}
|
||||
}
|
||||
|
||||
private commit() {
|
||||
@@ -226,7 +285,7 @@ export class Convo {
|
||||
|
||||
getSnapshot(): ConvoState {
|
||||
if (!this.snapshot) this.snapshot = this.generateSnapshot()
|
||||
logger.debug('Convo: snapshotted', {}, logger.DebugContext.convo)
|
||||
// logger.debug('Convo: snapshotted', {}, logger.DebugContext.convo)
|
||||
return this.snapshot
|
||||
}
|
||||
|
||||
@@ -238,6 +297,8 @@ export class Convo {
|
||||
items: [],
|
||||
convo: undefined,
|
||||
error: undefined,
|
||||
sender: undefined,
|
||||
recipients: undefined,
|
||||
isFetchingHistory: this.isFetchingHistory,
|
||||
deleteMessage: undefined,
|
||||
sendMessage: undefined,
|
||||
@@ -246,13 +307,14 @@ export class Convo {
|
||||
}
|
||||
case ConvoStatus.Suspended:
|
||||
case ConvoStatus.Backgrounded:
|
||||
case ConvoStatus.Resuming:
|
||||
case ConvoStatus.Ready: {
|
||||
return {
|
||||
status: this.status,
|
||||
items: this.getItems(),
|
||||
convo: this.convo!,
|
||||
error: undefined,
|
||||
sender: this.sender!,
|
||||
recipients: this.recipients!,
|
||||
isFetchingHistory: this.isFetchingHistory,
|
||||
deleteMessage: this.deleteMessage,
|
||||
sendMessage: this.sendMessage,
|
||||
@@ -265,6 +327,8 @@ export class Convo {
|
||||
items: [],
|
||||
convo: undefined,
|
||||
error: this.error,
|
||||
sender: undefined,
|
||||
recipients: undefined,
|
||||
isFetchingHistory: false,
|
||||
deleteMessage: undefined,
|
||||
sendMessage: undefined,
|
||||
@@ -277,6 +341,8 @@ export class Convo {
|
||||
items: [],
|
||||
convo: undefined,
|
||||
error: undefined,
|
||||
sender: undefined,
|
||||
recipients: undefined,
|
||||
isFetchingHistory: false,
|
||||
deleteMessage: undefined,
|
||||
sendMessage: undefined,
|
||||
@@ -286,87 +352,310 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
async init() {
|
||||
logger.debug('Convo: init', {}, logger.DebugContext.convo)
|
||||
dispatch(action: ConvoDispatch) {
|
||||
const prevStatus = this.status
|
||||
|
||||
if (this.status === ConvoStatus.Uninitialized) {
|
||||
try {
|
||||
this.status = ConvoStatus.Initializing
|
||||
this.commit()
|
||||
|
||||
await this.refreshConvo()
|
||||
this.status = ConvoStatus.Ready
|
||||
this.commit()
|
||||
|
||||
await this.fetchMessageHistory()
|
||||
|
||||
this.pollEvents()
|
||||
} catch (e) {
|
||||
this.error = e
|
||||
this.status = ConvoStatus.Error
|
||||
this.commit()
|
||||
switch (this.status) {
|
||||
case ConvoStatus.Uninitialized: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Init: {
|
||||
this.status = ConvoStatus.Initializing
|
||||
this.setup()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Convo: cannot init from ${this.status}`)
|
||||
}
|
||||
}
|
||||
|
||||
async resume() {
|
||||
logger.debug('Convo: resume', {}, logger.DebugContext.convo)
|
||||
|
||||
if (
|
||||
this.status === ConvoStatus.Suspended ||
|
||||
this.status === ConvoStatus.Backgrounded
|
||||
) {
|
||||
try {
|
||||
this.status = ConvoStatus.Resuming
|
||||
this.commit()
|
||||
|
||||
await this.refreshConvo()
|
||||
this.status = ConvoStatus.Ready
|
||||
this.commit()
|
||||
|
||||
await this.fetchMessageHistory()
|
||||
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.pollEvents()
|
||||
} catch (e) {
|
||||
// TODO handle errors in one place
|
||||
this.error = e
|
||||
this.status = ConvoStatus.Error
|
||||
this.commit()
|
||||
case ConvoStatus.Initializing: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Ready: {
|
||||
this.status = ConvoStatus.Ready
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.fetchMessageHistory().then(() => {
|
||||
this.restartPoll()
|
||||
})
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Background: {
|
||||
this.status = ConvoStatus.Backgrounded
|
||||
this.pollInterval = BACKGROUND_POLL_INTERVAL
|
||||
this.fetchMessageHistory().then(() => {
|
||||
this.restartPoll()
|
||||
})
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Suspend: {
|
||||
this.status = ConvoStatus.Suspended
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Convo: cannot resume from ${this.status}`)
|
||||
case ConvoStatus.Ready: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
this.refreshConvo()
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Background: {
|
||||
this.status = ConvoStatus.Backgrounded
|
||||
this.pollInterval = BACKGROUND_POLL_INTERVAL
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Suspend: {
|
||||
this.status = ConvoStatus.Suspended
|
||||
this.cancelNextPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
this.cancelNextPoll()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Backgrounded: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
this.status = ConvoStatus.Ready
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.refreshConvo()
|
||||
// TODO truncate history if needed
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Suspend: {
|
||||
this.status = ConvoStatus.Suspended
|
||||
this.cancelNextPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
this.cancelNextPoll()
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Suspended: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Init: {
|
||||
this.status = ConvoStatus.Ready
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.refreshConvo()
|
||||
// TODO truncate history if needed
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
this.status = ConvoStatus.Ready
|
||||
this.pollInterval = ACTIVE_POLL_INTERVAL
|
||||
this.refreshConvo()
|
||||
this.restartPoll()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case ConvoStatus.Error: {
|
||||
switch (action.event) {
|
||||
case ConvoDispatchEvent.Init: {
|
||||
this.reset()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Resume: {
|
||||
this.reset()
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Suspend: {
|
||||
this.status = ConvoStatus.Suspended
|
||||
break
|
||||
}
|
||||
case ConvoDispatchEvent.Error: {
|
||||
this.status = ConvoStatus.Error
|
||||
this.error = action.payload
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async background() {
|
||||
logger.debug('Convo: backgrounded', {}, logger.DebugContext.convo)
|
||||
this.status = ConvoStatus.Backgrounded
|
||||
this.pollInterval = BACKGROUND_POLL_INTERVAL
|
||||
logger.debug(
|
||||
`Convo: dispatch '${action.event}'`,
|
||||
{
|
||||
id: this.id,
|
||||
prev: prevStatus,
|
||||
next: this.status,
|
||||
},
|
||||
logger.DebugContext.convo,
|
||||
)
|
||||
|
||||
this.commit()
|
||||
}
|
||||
|
||||
async suspend() {
|
||||
logger.debug('Convo: suspended', {}, logger.DebugContext.convo)
|
||||
this.status = ConvoStatus.Suspended
|
||||
this.commit()
|
||||
private reset() {
|
||||
this.convo = undefined
|
||||
this.sender = undefined
|
||||
this.recipients = undefined
|
||||
this.snapshot = undefined
|
||||
|
||||
this.status = ConvoStatus.Uninitialized
|
||||
this.error = undefined
|
||||
this.historyCursor = undefined
|
||||
this.eventsCursor = undefined
|
||||
|
||||
this.pastMessages = new Map()
|
||||
this.newMessages = new Map()
|
||||
this.pendingMessages = new Map()
|
||||
this.deletedMessages = new Set()
|
||||
this.footerItems = new Map()
|
||||
this.headerItems = new Map()
|
||||
|
||||
this.dispatch({event: ConvoDispatchEvent.Init})
|
||||
}
|
||||
|
||||
private async setup() {
|
||||
try {
|
||||
const {convo, sender, recipients} = await this.fetchConvo()
|
||||
|
||||
this.convo = convo
|
||||
this.sender = sender
|
||||
this.recipients = recipients
|
||||
|
||||
/*
|
||||
* Some validation prior to `Ready` status
|
||||
*/
|
||||
if (!this.convo) {
|
||||
throw new Error('Convo: could not find convo')
|
||||
}
|
||||
if (!this.sender) {
|
||||
throw new Error('Convo: could not find sender in convo')
|
||||
}
|
||||
if (!this.recipients) {
|
||||
throw new Error('Convo: could not find recipients in convo')
|
||||
}
|
||||
|
||||
// await new Promise(y => setTimeout(y, 2000))
|
||||
// throw new Error('UNCOMMENT TO TEST INIT FAILURE')
|
||||
this.dispatch({event: ConvoDispatchEvent.Ready})
|
||||
} catch (e: any) {
|
||||
logger.error('Convo: setup() failed')
|
||||
|
||||
this.dispatch({
|
||||
event: ConvoDispatchEvent.Error,
|
||||
payload: {
|
||||
exception: e,
|
||||
code: ConvoErrorCode.InitFailed,
|
||||
retry: () => {
|
||||
this.reset()
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.dispatch({event: ConvoDispatchEvent.Init})
|
||||
}
|
||||
|
||||
resume() {
|
||||
this.dispatch({event: ConvoDispatchEvent.Resume})
|
||||
}
|
||||
|
||||
background() {
|
||||
this.dispatch({event: ConvoDispatchEvent.Background})
|
||||
}
|
||||
|
||||
suspend() {
|
||||
this.dispatch({event: ConvoDispatchEvent.Suspend})
|
||||
DEBUG_ACTIVE_CHAT = undefined
|
||||
}
|
||||
|
||||
private pendingFetchConvo:
|
||||
| Promise<{
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
sender: AppBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[]
|
||||
}>
|
||||
| undefined
|
||||
async fetchConvo() {
|
||||
if (this.pendingFetchConvo) return this.pendingFetchConvo
|
||||
|
||||
this.pendingFetchConvo = new Promise<{
|
||||
convo: ChatBskyConvoDefs.ConvoView
|
||||
sender: AppBskyActorDefs.ProfileViewBasic | undefined
|
||||
recipients: AppBskyActorDefs.ProfileViewBasic[]
|
||||
}>(async (resolve, reject) => {
|
||||
try {
|
||||
const response = await this.agent.api.chat.bsky.convo.getConvo(
|
||||
{
|
||||
convoId: this.convoId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const convo = response.data.convo
|
||||
|
||||
resolve({
|
||||
convo,
|
||||
sender: convo.members.find(m => m.did === this.__tempFromUserDid),
|
||||
recipients: convo.members.filter(
|
||||
m => m.did !== this.__tempFromUserDid,
|
||||
),
|
||||
})
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
this.pendingFetchConvo = undefined
|
||||
}
|
||||
})
|
||||
|
||||
return this.pendingFetchConvo
|
||||
}
|
||||
|
||||
async refreshConvo() {
|
||||
const response = await this.agent.api.chat.bsky.convo.getConvo(
|
||||
{
|
||||
convoId: this.convoId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
try {
|
||||
const {convo, sender, recipients} = await this.fetchConvo()
|
||||
// throw new Error('UNCOMMENT TO TEST REFRESH FAILURE')
|
||||
this.convo = convo || this.convo
|
||||
this.sender = sender || this.sender
|
||||
this.recipients = recipients || this.recipients
|
||||
} catch (e: any) {
|
||||
logger.error(`Convo: failed to refresh convo`)
|
||||
|
||||
this.footerItems.set(ConvoItemError.Network, {
|
||||
type: 'error-recoverable',
|
||||
key: ConvoItemError.Network,
|
||||
code: ConvoItemError.Network,
|
||||
retry: () => {
|
||||
this.footerItems.delete(ConvoItemError.Network)
|
||||
this.resume()
|
||||
},
|
||||
},
|
||||
)
|
||||
this.convo = response.data.convo
|
||||
this.sender = this.convo.members.find(m => m.did === this.__tempFromUserDid)
|
||||
})
|
||||
this.commit()
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMessageHistory() {
|
||||
@@ -386,7 +675,7 @@ export class Convo {
|
||||
* If we've rendered a retry state for history fetching, exit. Upon retry,
|
||||
* this will be removed and we'll try again.
|
||||
*/
|
||||
if (this.headerItems.has(ConvoError.HistoryFailed)) return
|
||||
if (this.headerItems.has(ConvoItemError.HistoryFailed)) return
|
||||
|
||||
try {
|
||||
this.isFetchingHistory = true
|
||||
@@ -435,12 +724,12 @@ export class Convo {
|
||||
} catch (e: any) {
|
||||
logger.error('Convo: failed to fetch message history')
|
||||
|
||||
this.headerItems.set(ConvoError.HistoryFailed, {
|
||||
this.headerItems.set(ConvoItemError.HistoryFailed, {
|
||||
type: 'error-recoverable',
|
||||
key: ConvoError.HistoryFailed,
|
||||
code: ConvoError.HistoryFailed,
|
||||
key: ConvoItemError.HistoryFailed,
|
||||
code: ConvoItemError.HistoryFailed,
|
||||
retry: () => {
|
||||
this.headerItems.delete(ConvoError.HistoryFailed)
|
||||
this.headerItems.delete(ConvoItemError.HistoryFailed)
|
||||
this.fetchMessageHistory()
|
||||
},
|
||||
})
|
||||
@@ -450,85 +739,134 @@ export class Convo {
|
||||
}
|
||||
}
|
||||
|
||||
private async pollEvents() {
|
||||
if (
|
||||
this.status === ConvoStatus.Ready ||
|
||||
this.status === ConvoStatus.Backgrounded
|
||||
) {
|
||||
if (this.pendingEventIngestion) return
|
||||
private restartPoll() {
|
||||
this.cancelNextPoll()
|
||||
this.pollLatestEvents()
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
this.pendingEventIngestion = this.ingestLatestEvents()
|
||||
await this.pendingEventIngestion
|
||||
this.pendingEventIngestion = undefined
|
||||
this.pollEvents()
|
||||
private cancelNextPoll() {
|
||||
if (this.nextPoll) clearTimeout(this.nextPoll)
|
||||
}
|
||||
|
||||
private pollLatestEvents() {
|
||||
/*
|
||||
* Uncomment to view poll events
|
||||
*/
|
||||
logger.debug('Convo: poll events', {id: this.id}, logger.DebugContext.convo)
|
||||
|
||||
try {
|
||||
this.fetchLatestEvents().then(({events}) => {
|
||||
this.applyLatestEvents(events)
|
||||
})
|
||||
this.nextPoll = setTimeout(() => {
|
||||
this.pollLatestEvents()
|
||||
}, this.pollInterval)
|
||||
} catch (e: any) {
|
||||
logger.error('Convo: poll events failed')
|
||||
|
||||
this.cancelNextPoll()
|
||||
|
||||
this.footerItems.set(ConvoItemError.PollFailed, {
|
||||
type: 'error-recoverable',
|
||||
key: ConvoItemError.PollFailed,
|
||||
code: ConvoItemError.PollFailed,
|
||||
retry: () => {
|
||||
this.footerItems.delete(ConvoItemError.PollFailed)
|
||||
this.commit()
|
||||
this.pollLatestEvents()
|
||||
},
|
||||
})
|
||||
|
||||
this.commit()
|
||||
}
|
||||
}
|
||||
|
||||
async ingestLatestEvents() {
|
||||
const response = await this.agent.api.chat.bsky.convo.getLog(
|
||||
{
|
||||
cursor: this.eventsCursor,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
},
|
||||
)
|
||||
const {logs} = response.data
|
||||
private pendingFetchLatestEvents:
|
||||
| Promise<{
|
||||
events: ChatBskyConvoGetLog.OutputSchema['logs']
|
||||
}>
|
||||
| undefined
|
||||
async fetchLatestEvents() {
|
||||
if (this.pendingFetchLatestEvents) return this.pendingFetchLatestEvents
|
||||
|
||||
this.pendingFetchLatestEvents = new Promise<{
|
||||
events: ChatBskyConvoGetLog.OutputSchema['logs']
|
||||
}>(async (resolve, reject) => {
|
||||
try {
|
||||
// throw new Error('UNCOMMENT TO TEST POLL FAILURE')
|
||||
const response = await this.agent.api.chat.bsky.convo.getLog(
|
||||
{
|
||||
cursor: this.eventsCursor,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.__tempFromUserDid,
|
||||
},
|
||||
},
|
||||
)
|
||||
const {logs} = response.data
|
||||
resolve({events: logs})
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
} finally {
|
||||
this.pendingFetchLatestEvents = undefined
|
||||
}
|
||||
})
|
||||
|
||||
return this.pendingFetchLatestEvents
|
||||
}
|
||||
|
||||
private applyLatestEvents(events: ChatBskyConvoGetLog.OutputSchema['logs']) {
|
||||
let needsCommit = false
|
||||
|
||||
for (const log of logs) {
|
||||
for (const ev of events) {
|
||||
/*
|
||||
* If there's a rev, we should handle it. If there's not a rev, we don't
|
||||
* know what it is.
|
||||
*/
|
||||
if (typeof log.rev === 'string') {
|
||||
if (typeof ev.rev === 'string') {
|
||||
/*
|
||||
* We only care about new events
|
||||
*/
|
||||
if (log.rev > (this.eventsCursor = this.eventsCursor || log.rev)) {
|
||||
if (ev.rev > (this.eventsCursor = this.eventsCursor || ev.rev)) {
|
||||
/*
|
||||
* Update rev regardless of if it's a log type we care about or not
|
||||
* Update rev regardless of if it's a ev type we care about or not
|
||||
*/
|
||||
this.eventsCursor = log.rev
|
||||
this.eventsCursor = ev.rev
|
||||
|
||||
/*
|
||||
* This is VERY important. We don't want to insert any messages from
|
||||
* your other chats.
|
||||
*/
|
||||
if (log.convoId !== this.convoId) continue
|
||||
if (ev.convoId !== this.convoId) continue
|
||||
|
||||
if (
|
||||
ChatBskyConvoDefs.isLogCreateMessage(log) &&
|
||||
ChatBskyConvoDefs.isMessageView(log.message)
|
||||
ChatBskyConvoDefs.isLogCreateMessage(ev) &&
|
||||
ChatBskyConvoDefs.isMessageView(ev.message)
|
||||
) {
|
||||
if (this.newMessages.has(log.message.id)) {
|
||||
// Trust the log as the source of truth on ordering
|
||||
this.newMessages.delete(log.message.id)
|
||||
if (this.newMessages.has(ev.message.id)) {
|
||||
// Trust the ev as the source of truth on ordering
|
||||
this.newMessages.delete(ev.message.id)
|
||||
}
|
||||
this.newMessages.set(log.message.id, log.message)
|
||||
this.newMessages.set(ev.message.id, ev.message)
|
||||
needsCommit = true
|
||||
} else if (
|
||||
ChatBskyConvoDefs.isLogDeleteMessage(log) &&
|
||||
ChatBskyConvoDefs.isDeletedMessageView(log.message)
|
||||
ChatBskyConvoDefs.isLogDeleteMessage(ev) &&
|
||||
ChatBskyConvoDefs.isDeletedMessageView(ev.message)
|
||||
) {
|
||||
/*
|
||||
* Update if we have this in state. If we don't, don't worry about it.
|
||||
*/
|
||||
if (this.pastMessages.has(log.message.id)) {
|
||||
if (this.pastMessages.has(ev.message.id)) {
|
||||
/*
|
||||
* For now, we remove deleted messages from the thread, if we receive one.
|
||||
*
|
||||
* To support them, it'd look something like this:
|
||||
* this.pastMessages.set(log.message.id, log.message)
|
||||
* this.pastMessages.set(ev.message.id, ev.message)
|
||||
*/
|
||||
this.pastMessages.delete(log.message.id)
|
||||
this.newMessages.delete(log.message.id)
|
||||
this.deletedMessages.delete(log.message.id)
|
||||
this.pastMessages.delete(ev.message.id)
|
||||
this.newMessages.delete(ev.message.id)
|
||||
this.deletedMessages.delete(ev.message.id)
|
||||
needsCommit = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, {useContext, useState, useSyncExternalStore} from 'react'
|
||||
import {AppState} from 'react-native'
|
||||
import {BskyAgent} from '@atproto-labs/api'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {useFocusEffect, useIsFocused} from '@react-navigation/native'
|
||||
|
||||
import {Convo, ConvoParams, ConvoState} from '#/state/messages/convo'
|
||||
import {useAgent} from '#/state/session'
|
||||
@@ -20,6 +21,7 @@ export function ChatProvider({
|
||||
children,
|
||||
convoId,
|
||||
}: Pick<ConvoParams, 'convoId'> & {children: React.ReactNode}) {
|
||||
const isScreenFocused = useIsFocused()
|
||||
const {serviceUrl} = useDmServiceUrlStorage()
|
||||
const {getAgent} = useAgent()
|
||||
const [convo] = useState(
|
||||
@@ -44,5 +46,23 @@ export function ChatProvider({
|
||||
}, [convo]),
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleAppStateChange = (nextAppState: string) => {
|
||||
if (isScreenFocused) {
|
||||
if (nextAppState === 'active') {
|
||||
convo.resume()
|
||||
} else {
|
||||
convo.background()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sub = AppState.addEventListener('change', handleAppStateChange)
|
||||
|
||||
return () => {
|
||||
sub.remove()
|
||||
}
|
||||
}, [convo, isScreenFocused])
|
||||
|
||||
return <ChatContext.Provider value={service}>{children}</ChatContext.Provider>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user