diff --git a/src/status_im/chat/models/loading.cljs b/src/status_im/chat/models/loading.cljs index 8fea1dc574..966b182a5e 100644 --- a/src/status_im/chat/models/loading.cljs +++ b/src/status_im/chat/models/loading.cljs @@ -4,7 +4,6 @@ [status-im2.constants :as constants] [status-im.data-store.chats :as data-store.chats] [status-im.data-store.messages :as data-store.messages] - [status-im2.contexts.activity-center.events :as activity-center] [taoensso.timbre :as log] [utils.re-frame :as rf])) @@ -69,15 +68,15 @@ (rf/defn handle-mark-all-read-successful {:events [::mark-all-read-successful]} - [cofx] - (activity-center/notifications-fetch-unread-count cofx)) + [_] + {:dispatch [:activity-center.notifications/fetch-unread-count]}) (rf/defn handle-mark-all-read-in-community-successful {:events [::mark-all-read-in-community-successful]} [{:keys [db] :as cofx} chat-ids] (rf/merge cofx - {:db (reduce mark-chat-all-read db chat-ids)} - (activity-center/notifications-fetch-unread-count))) + {:db (reduce mark-chat-all-read db chat-ids) + :dispatch [:activity-center.notifications/fetch-unread-count]})) (rf/defn handle-mark-all-read {:events [:chat.ui/mark-all-read-pressed :chat/mark-all-as-read]} diff --git a/src/status_im2/contexts/activity_center/events.cljs b/src/status_im2/contexts/activity_center/events.cljs index 5083425e3a..6b62ee6640 100644 --- a/src/status_im2/contexts/activity_center/events.cljs +++ b/src/status_im2/contexts/activity_center/events.cljs @@ -1,6 +1,8 @@ (ns status-im2.contexts.activity-center.events (:require [status-im.data-store.activities :as data-store.activities] + [status-im.data-store.chats :as data-store.chats] [status-im2.contexts.activity-center.notification-types :as types] + [status-im2.contexts.chat.events :as chat.events] [taoensso.timbre :as log] [utils.re-frame :as rf])) @@ -41,6 +43,12 @@ ;;;; Notification reconciliation +(defn- notification-type->filter-type + [type] + (if (some types/membership [type]) + types/membership + type)) + (defn- update-notifications "Insert `new-notifications` in `db-notifications`. @@ -50,8 +58,9 @@ If the number of existing notifications cached in the app db becomes ~excessively~ big, this implementation will probably need to be revisited." [db-notifications new-notifications] - (reduce (fn [acc {:keys [id type read] :as notification}] - (let [remove-notification (fn [data] + (reduce (fn [acc {:keys [id read] :as notification}] + (let [filter-type (notification-type->filter-type (:type notification)) + remove-notification (fn [data] (remove #(= id (:id %)) data)) insert-and-sort (fn [data] (->> notification @@ -59,16 +68,16 @@ (sort-by (juxt :timestamp :id)) reverse))] (as-> acc $ - (update-in $ [type :all :data] remove-notification) + (update-in $ [filter-type :all :data] remove-notification) (update-in $ [types/no-type :all :data] remove-notification) - (update-in $ [type :unread :data] remove-notification) + (update-in $ [filter-type :unread :data] remove-notification) (update-in $ [types/no-type :unread :data] remove-notification) - (if (or (:dismissed notification) (:accepted notification)) + (if (:dismissed notification) $ (cond-> (-> $ - (update-in [type :all :data] insert-and-sort) + (update-in [filter-type :all :data] insert-and-sort) (update-in [types/no-type :all :data] insert-and-sort)) - (not read) (update-in [type :unread :data] insert-and-sort) + (not read) (update-in [filter-type :unread :data] insert-and-sort) (not read) (update-in [types/no-type :unread :data] insert-and-sort)))))) db-notifications new-notifications)) @@ -77,10 +86,11 @@ {:events [:activity-center.notifications/reconcile]} [{:keys [db]} new-notifications] (when (seq new-notifications) - {:db (update-in db - [:activity-center :notifications] - update-notifications - new-notifications)})) + {:db (update-in db + [:activity-center :notifications] + update-notifications + new-notifications) + :dispatch [:activity-center.notifications/fetch-unread-count]})) (rf/defn notifications-reconcile-from-response {:events [:activity-center/reconcile-notifications-from-response]} @@ -143,6 +153,46 @@ [cofx notification] (notifications-reconcile cofx [(assoc notification :read true)])) +;;;; Acceptance/dismissal + +(rf/defn accept-notification + {:events [:activity-center.notifications/accept]} + [{:keys [db]} notification-id] + {:json-rpc/call [{:method "wakuext_acceptActivityCenterNotifications" + :params [[notification-id]] + :on-success #(rf/dispatch [:activity-center.notifications/accept-success + notification-id %]) + :on-error #(rf/dispatch [:activity-center/process-notification-failure + notification-id + :notification/accept + %])}]}) + +(rf/defn accept-notification-success + {:events [:activity-center.notifications/accept-success]} + [{:keys [db] :as cofx} notification-id {:keys [chats]}] + (let [notification (get-notification db notification-id)] + (rf/merge cofx + (chat.events/ensure-chats (map data-store.chats/<-rpc chats)) + (notifications-reconcile [(assoc notification :read true :accepted true)])))) + +(rf/defn dismiss-notification + {:events [:activity-center.notifications/dismiss]} + [{:keys [db]} notification-id] + {:json-rpc/call [{:method "wakuext_dismissActivityCenterNotifications" + :params [[notification-id]] + :on-success #(rf/dispatch [:activity-center.notifications/dismiss-success + notification-id]) + :on-error #(rf/dispatch [:activity-center/process-notification-failure + notification-id + :notification/dismiss + %])}]}) + +(rf/defn dismiss-notification-success + {:events [:activity-center.notifications/dismiss-success]} + [{:keys [db] :as cofx} notification-id] + (let [notification (get-notification db notification-id)] + (notifications-reconcile cofx [(assoc notification :dismissed true)]))) + ;;;; Contact verification (rf/defn contact-verification-decline @@ -213,15 +263,35 @@ :all status-all 99)) +(defn filter-type->rpc-param + [filter-type] + (cond + (coll? filter-type) + filter-type + + ;; A "no-type" notification shouldn't be sent to the backend. If, for + ;; instance, the mobile client needs notifications of any type (as in the + ;; `All` tab), then just don't filter by type at all. + (= types/no-type filter-type) + nil + + :else + [filter-type])) + (rf/defn notifications-fetch [{:keys [db]} {:keys [cursor per-page filter-type filter-status reset-data?]}] (when-not (get-in db [:activity-center :notifications filter-type filter-status :loading?]) - (let [per-page (or per-page (defaults :notifications-per-page))] + (let [per-page (or per-page (defaults :notifications-per-page)) + accepted? true] {:db (assoc-in db [:activity-center :notifications filter-type filter-status :loading?] true) :json-rpc/call [{:method "wakuext_activityCenterNotificationsBy" - :params [cursor per-page filter-type (status filter-status)] + :params [cursor + per-page + (filter-type->rpc-param filter-type) + (status filter-status) + accepted?] :on-success #(rf/dispatch [:activity-center.notifications/fetch-success filter-type filter-status reset-data? %]) :on-error #(rf/dispatch [:activity-center.notifications/fetch-error @@ -295,7 +365,7 @@ (rf/defn notifications-fetch-unread-count {:events [:activity-center.notifications/fetch-unread-count]} [_] - {:json-rpc/call [{:method "wakuext_unreadActivityCenterNotificationsCount" + {:json-rpc/call [{:method "wakuext_unreadAndAcceptedActivityCenterNotificationsCount" :params [] :on-success #(rf/dispatch [:activity-center.notifications/fetch-unread-count-success %]) diff --git a/src/status_im2/contexts/activity_center/events_test.cljs b/src/status_im2/contexts/activity_center/events_test.cljs index b1738ed6da..7f0ad84aff 100644 --- a/src/status_im2/contexts/activity_center/events_test.cljs +++ b/src/status_im2/contexts/activity_center/events_test.cljs @@ -141,6 +141,74 @@ :filter {:type types/one-to-one-chat :status :all}}]))}))) +;;;; Acceptance/dismissal + +(deftest notification-acceptance-test + (testing "marks notification as accepted and read, then reconciles" + (h/run-test-sync + (setup) + (let [notif-1 {:id "0x1" :type types/private-group-chat} + notif-2 {:id "0x2" :type types/private-group-chat} + notif-3 {:id "0x3" :type types/admin} + notif-2-new (assoc notif-2 :accepted true :read true)] + (h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2)) + (rf/dispatch [:test/assoc-in [:activity-center] + {:notifications {types/membership + {:unread {:cursor "" :data [notif-2 notif-1]}} + + types/admin + {:all {:cursor "" :data [notif-3]}}} + :filter {:type types/membership + :status :unread}}]) + + (rf/dispatch [:activity-center.notifications/accept (:id notif-2)]) + + (is (= {types/no-type {:all {:data [notif-2-new]} + :unread {:data []}} + types/membership {:all {:data [notif-2-new]} + :unread {:cursor "" :data [notif-1]}} + types/admin {:all {:cursor "" :data [notif-3]}}} + (get-in (h/db) [:activity-center :notifications])))))) + + (testing "logs on failure" + (test-log-on-failure + {:notification-id notification-id + :event [:activity-center.notifications/accept notification-id] + :action :notification/accept}))) + +(deftest notification-dismissal-test + (testing "dismisses notification and removes from app db" + (h/run-test-sync + (setup) + (let [notif-1 {:id "0x1" :type types/private-group-chat} + notif-2 {:id "0x2" :type types/admin}] + (h/stub-fx-with-callbacks :json-rpc/call :on-success (constantly notif-2)) + (rf/dispatch [:test/assoc-in [:activity-center] + {:notifications {types/no-type + {:all {:cursor "" :data [notif-2 notif-1]}} + + types/membership + {:unread {:cursor "" :data [notif-1]}}} + :filter {:type types/membership + :status :unread}}]) + + (rf/dispatch [:activity-center.notifications/dismiss (:id notif-1)]) + + (is (= {types/no-type + {:all {:cursor "" :data [notif-2]} + :unread {:data []}} + + types/membership + {:all {:data []} + :unread {:cursor "" :data []}}} + (get-in (h/db) [:activity-center :notifications])))))) + + (testing "logs on failure" + (test-log-on-failure + {:notification-id notification-id + :event [:activity-center.notifications/dismiss notification-id] + :action :notification/dismiss}))) + ;;;; Contact verification (def contact-verification-rpc-response @@ -287,45 +355,44 @@ (is (= notifications (get-in (h/db) [:activity-center :notifications])))))) - (testing "removes dismissed or accepted notifications" + (testing "removes dismissed notifications" (h/run-test-sync (setup) (let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat} notif-2 {:id "0x2" :read false :type types/one-to-one-chat} - notif-3 {:id "0x3" :read false :type types/one-to-one-chat} - notif-4 {:id "0x4" :read false :type types/private-group-chat} - notif-5 {:id "0x5" :read true :type types/private-group-chat} - notif-6 {:id "0x6" :read false :type types/private-group-chat}] + notif-3 {:id "0x3" :read false :type types/system} + notif-4 {:id "0x4" :read true :type types/system} + notif-5 {:id "0x5" :read false :type types/system :accepted true}] (rf/dispatch [:test/assoc-in [:activity-center :notifications] {types/one-to-one-chat {:all {:cursor "" :data [notif-1 notif-2]} - :unread {:cursor "" :data [notif-3]}} - types/private-group-chat - {:unread {:cursor "" :data [notif-4 notif-6]}}}]) + :unread {:cursor "" :data [notif-2]}} + types/system + {:all {:cursor "" :data [notif-4]} + :unread {:cursor "" :data [notif-3 notif-5]}}}]) (rf/dispatch [:activity-center.notifications/reconcile [(assoc notif-1 :dismissed true) - (assoc notif-3 :accepted true) (assoc notif-4 :dismissed true) notif-5]]) (is (= {types/no-type {:all {:data [notif-5]} - :unread {:data []}} + :unread {:data [notif-5]}} types/one-to-one-chat {:all {:cursor "" :data [notif-2]} - :unread {:cursor "" :data []}} - types/private-group-chat - {:all {:data [notif-5]} - :unread {:cursor "" :data [notif-6]}}} + :unread {:cursor "" :data [notif-2]}} + types/system + {:all {:cursor "" :data [notif-5]} + :unread {:cursor "" :data [notif-5 notif-3]}}} (get-in (h/db) [:activity-center :notifications])))))) (testing "replaces old notifications with newly arrived ones" (h/run-test-sync (setup) (let [notif-1 {:id "0x1" :read true :type types/one-to-one-chat} - notif-4 {:id "0x4" :read false :type types/private-group-chat} - notif-6 {:id "0x6" :read false :type types/private-group-chat} + notif-4 {:id "0x4" :read false :type types/system} + notif-6 {:id "0x6" :read false :type types/system} new-notif-1 (assoc notif-1 :last-message {}) new-notif-4 (assoc notif-4 :author "0xabc")] (rf/dispatch [:test/assoc-in [:activity-center :notifications] @@ -334,7 +401,7 @@ :unread {:cursor "" :data [notif-4 notif-6]}} types/one-to-one-chat {:all {:cursor "" :data [notif-1]}} - types/private-group-chat + types/system {:unread {:cursor "" :data [notif-4 notif-6]}}}]) (rf/dispatch [:activity-center.notifications/reconcile [new-notif-1 new-notif-4 notif-6]]) @@ -345,7 +412,7 @@ types/one-to-one-chat {:all {:cursor "" :data [new-notif-1]} :unread {:data []}} - types/private-group-chat + types/system {:all {:data [notif-6 new-notif-4]} :unread {:cursor "" :data [notif-6 new-notif-4]}}} (get-in (h/db) [:activity-center :notifications])))))) @@ -370,6 +437,26 @@ :unread {:data [new-notif-1]}}} (get-in (h/db) [:activity-center :notifications])))))) + (testing "membership notifications" + (h/run-test-sync + (setup) + (let [notif {:read false + :dismissed false + :accepted false + :type types/private-group-chat + :id "0x7" + :timestamp 1673445663000}] + (rf/dispatch [:activity-center.notifications/reconcile [notif]]) + + (is (= {types/no-type + {:all {:data [notif]} + :unread {:data [notif]}} + + types/membership + {:all {:data [notif]} + :unread {:data [notif]}}} + (get-in (h/db) [:activity-center :notifications])))))) + ;; Sorting by timestamp and ID is compatible with what the backend does when ;; returning paginated results. (testing "sorts notifications by timestamp and id in descending order" @@ -595,7 +682,7 @@ (:db actual))) (is (= {:method "wakuext_activityCenterNotificationsBy" - :params ["" per-page types/contact-request activity-center/status-unread]} + :params ["" per-page [types/contact-request] activity-center/status-unread true]} (-> actual :json-rpc/call first @@ -611,6 +698,6 @@ (rf/dispatch [:activity-center.notifications/fetch-unread-count]) - (is (= "wakuext_unreadActivityCenterNotificationsCount" + (is (= "wakuext_unreadAndAcceptedActivityCenterNotificationsCount" (get-in @spy-queue [0 :args 0 :method]))) (is (= 9 (get-in (h/db) [:activity-center :unread-count]))))))) diff --git a/src/status_im2/contexts/activity_center/notification/admin/view.cljs b/src/status_im2/contexts/activity_center/notification/admin/view.cljs index e7cef35332..92551e328d 100644 --- a/src/status_im2/contexts/activity_center/notification/admin/view.cljs +++ b/src/status_im2/contexts/activity_center/notification/admin/view.cljs @@ -1,11 +1,11 @@ (ns status-im2.contexts.activity-center.notification.admin.view - (:require [utils.i18n :as i18n] - [quo2.core :as quo] + (:require [quo2.core :as quo] [quo2.foundations.colors :as colors] [status-im2.constants :as constants] [status-im2.contexts.activity-center.notification.common.style :as style] [status-im2.contexts.activity-center.notification.common.view :as common] [utils.datetime :as datetime] + [utils.i18n :as i18n] [utils.re-frame :as rf])) (defn view diff --git a/src/status_im2/contexts/activity_center/notification/contact_request/view.cljs b/src/status_im2/contexts/activity_center/notification/contact_request/view.cljs index 9c5d45a5ae..fe0fc0103f 100644 --- a/src/status_im2/contexts/activity_center/notification/contact_request/view.cljs +++ b/src/status_im2/contexts/activity_center/notification/contact_request/view.cljs @@ -1,10 +1,10 @@ (ns status-im2.contexts.activity-center.notification.contact-request.view - (:require [utils.i18n :as i18n] - [quo2.core :as quo] + (:require [quo2.core :as quo] [react-native.core :as rn] [status-im2.constants :as constants] - [utils.datetime :as datetime] [status-im2.contexts.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] [utils.re-frame :as rf])) (defn view diff --git a/src/status_im2/contexts/activity_center/notification/contact_verification/view.cljs b/src/status_im2/contexts/activity_center/notification/contact_verification/view.cljs index c4b2865c64..45a777764e 100644 --- a/src/status_im2/contexts/activity_center/notification/contact_verification/view.cljs +++ b/src/status_im2/contexts/activity_center/notification/contact_verification/view.cljs @@ -1,10 +1,10 @@ (ns status-im2.contexts.activity-center.notification.contact-verification.view (:require [clojure.string :as string] - [utils.i18n :as i18n] [quo2.core :as quo] [status-im2.constants :as constants] - [utils.datetime :as datetime] [status-im2.contexts.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] [utils.re-frame :as rf])) (defn- hide-bottom-sheet-and-dispatch diff --git a/src/status_im2/contexts/activity_center/notification/membership/view.cljs b/src/status_im2/contexts/activity_center/notification/membership/view.cljs new file mode 100644 index 0000000000..78c1f041c2 --- /dev/null +++ b/src/status_im2/contexts/activity_center/notification/membership/view.cljs @@ -0,0 +1,41 @@ +(ns status-im2.contexts.activity-center.notification.membership.view + (:require [quo2.core :as quo] + [react-native.core :as rn] + [status-im2.contexts.activity-center.notification.common.view :as common] + [utils.datetime :as datetime] + [utils.i18n :as i18n] + [utils.re-frame :as rf])) + +(defn pressable + [{:keys [accepted chat-id]} & children] + (if accepted + (into [rn/touchable-opacity + {:on-press (fn [] + (rf/dispatch [:hide-popover]) + (rf/dispatch [:chat.ui/navigate-to-chat-nav2 chat-id]))}] + children) + (into [:<>] children))) + +(defn view + [{:keys [id accepted author read timestamp chat-name chat-id]}] + [pressable {:accepted accepted :chat-id chat-id} + [quo/activity-log + (merge + {:title (i18n/label :t/added-to-group-chat) + :icon :i/add-user + :timestamp (datetime/timestamp->relative timestamp) + :unread? (not read) + :context [[common/user-avatar-tag author] + (i18n/label :t/added-you-to) + [quo/group-avatar-tag chat-name + {:size :small + :color :purple}]]} + (when-not accepted + {:button-2 {:label (i18n/label :t/accept) + :accessibility-label :accept-group-chat-invitation + :type :positive + :on-press #(rf/dispatch [:activity-center.notifications/accept id])} + :button-1 {:label (i18n/label :t/decline) + :accessibility-label :decline-group-chat-invitation + :type :danger + :on-press #(rf/dispatch [:activity-center.notifications/dismiss id])}}))]]) diff --git a/src/status_im2/contexts/activity_center/notification/mentions/view.cljs b/src/status_im2/contexts/activity_center/notification/mentions/view.cljs index 2e3081286c..1c5e1e3b21 100644 --- a/src/status_im2/contexts/activity_center/notification/mentions/view.cljs +++ b/src/status_im2/contexts/activity_center/notification/mentions/view.cljs @@ -1,12 +1,12 @@ (ns status-im2.contexts.activity-center.notification.mentions.view (:require [clojure.string :as string] - [utils.i18n :as i18n] [quo2.core :as quo] [quo2.foundations.colors :as colors] [react-native.core :as rn] - [utils.datetime :as datetime] [status-im2.contexts.activity-center.notification.common.view :as common] [status-im2.contexts.activity-center.notification.mentions.style :as style] + [utils.datetime :as datetime] + [utils.i18n :as i18n] [utils.re-frame :as rf])) (def tag-params diff --git a/src/status_im2/contexts/activity_center/notification/reply/view.cljs b/src/status_im2/contexts/activity_center/notification/reply/view.cljs index ef07cd8bc3..e1731e3359 100644 --- a/src/status_im2/contexts/activity_center/notification/reply/view.cljs +++ b/src/status_im2/contexts/activity_center/notification/reply/view.cljs @@ -1,6 +1,5 @@ (ns status-im2.contexts.activity-center.notification.reply.view - (:require [utils.i18n :as i18n] - [quo2.core :as quo] + (:require [quo2.core :as quo] [quo2.foundations.colors :as colors] [react-native.core :as rn] [status-im.ui2.screens.chat.messages.message :as old-message] @@ -8,6 +7,7 @@ [status-im2.contexts.activity-center.notification.common.view :as common] [status-im2.contexts.activity-center.notification.reply.style :as style] [utils.datetime :as datetime] + [utils.i18n :as i18n] [utils.re-frame :as rf])) (def tag-params diff --git a/src/status_im2/contexts/activity_center/notification_types.cljs b/src/status_im2/contexts/activity_center/notification_types.cljs index 69f0b31bc2..c2049eb111 100644 --- a/src/status_im2/contexts/activity_center/notification_types.cljs +++ b/src/status_im2/contexts/activity_center/notification_types.cljs @@ -11,5 +11,10 @@ ;; TODO: Replace with correct enum values once status-go implements them. (def ^:const tx 66612) -(def ^:const membership 66613) (def ^:const system 66614) + +(def ^:const membership + "Membership is like a logical group of notifications with different types, i.e. + it doesn't have a corresponding type in the backend. Think of the collection + as a composite key of actual types." + #{private-group-chat}) diff --git a/src/status_im2/contexts/activity_center/view.cljs b/src/status_im2/contexts/activity_center/view.cljs index 8e298a9ec4..e9204b9bde 100644 --- a/src/status_im2/contexts/activity_center/view.cljs +++ b/src/status_im2/contexts/activity_center/view.cljs @@ -9,6 +9,7 @@ [status-im2.contexts.activity-center.notification.contact-request.view :as contact-request] [status-im2.contexts.activity-center.notification.contact-verification.view :as contact-verification] + [status-im2.contexts.activity-center.notification.membership.view :as membership] [status-im2.contexts.activity-center.notification.mentions.view :as mentions] [status-im2.contexts.activity-center.notification.reply.view :as reply] [status-im2.contexts.activity-center.style :as style] @@ -98,24 +99,28 @@ [filter-selector-read-toggle]]]]) (defn render-notification - [notification index] + [{:keys [type] :as notification} index] [rn/view {:style (style/notification-container index)} - (case (:type notification) - types/contact-verification + (cond + (= type types/contact-verification) [contact-verification/view notification {}] - types/contact-request + (= type types/contact-request) [contact-request/view notification] - types/mention + (= type types/mention) [mentions/view notification] - types/reply + (= type types/reply) [reply/view notification] - types/admin + (= type types/admin) [admin/view notification] + (some types/membership [type]) + [membership/view notification] + + :else nil)]) (defn view diff --git a/status-go-version.json b/status-go-version.json index 68a79dde65..89f32980db 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -3,7 +3,7 @@ "_comment": "Instead use: scripts/update-status-go.sh ", "owner": "status-im", "repo": "status-go", - "version": "v0.122.1", - "commit-sha1": "d60c1d00ed93d4340ab06a32ce062dd7e67e67c0", - "src-sha256": "1452a1n5fijcpq56mxaaacjx4091mpxkq62qi1b2xiy91ll6ik5f" + "version": "v0.125.0", + "commit-sha1": "e40cbfc28f6195b31eb0f59cbd58fd1d77a12821", + "src-sha256": "0nmys3mf46pilmqrr937b0bc1qi9h51aamyswngy4qfb96nvky85" } diff --git a/translations/en.json b/translations/en.json index 9fe229500c..516c90f625 100644 --- a/translations/en.json +++ b/translations/en.json @@ -10,6 +10,8 @@ "accept-community-rules": "I agree with the community rules", "account-added": "Account added", "account-color": "Account color", + "added-to-group-chat": "Added to group chat", + "added-you-to": "added you to", "anyone": "Anyone", "messages-from-contacts-only-subtitle": "Only people you added as contacts can start a new chat with you or invite you to a group", "messages-gap-warning": "Some messages might be missing",