diff --git a/src/status_im/acquisition/install_referrer.cljs b/src/status_im/acquisition/install_referrer.cljs index 1050869bb2..a5630c55bd 100644 --- a/src/status_im/acquisition/install_referrer.cljs +++ b/src/status_im/acquisition/install_referrer.cljs @@ -1,32 +1,12 @@ (ns status-im.acquisition.install-referrer (:require [taoensso.timbre :as log] - [clojure.string :as cstr] - ["react-native-device-info" :refer [getInstallReferrer]])) - -(defn- split-param [param] - (-> - (cstr/split param #"=") - (concat (repeat "")) - (->> - (take 2)))) - -(defn- url-decode - [string] - (some-> string str (cstr/replace #"\+" "%20") (js/decodeURIComponent))) - -(defn- query->map - [qstr] - (when-not (cstr/blank? qstr) - (some->> (cstr/split qstr #"&") - seq - (mapcat split-param) - (map url-decode) - (apply hash-map)))) + ["react-native-device-info" :refer [getInstallReferrer]] + [status-im.utils.http :as http])) (defn parse-referrer "Google return query params for referral with all utm tags" [referrer] - (-> referrer query->map (get "referrer"))) + (-> referrer http/query->map (get "referrer"))) (defn get-referrer [cb] (-> (getInstallReferrer) diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index 7a2e1fd13c..f7a7c538b8 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -110,10 +110,12 @@ (defn map-chats [{:keys [db] :as cofx}] (fn [val] - (merge - (or (get (:chats db) (:chat-id val)) - (create-new-chat (:chat-id val) cofx)) - val))) + (assoc + (merge + (or (get (:chats db) (:chat-id val)) + (create-new-chat (:chat-id val) cofx)) + val) + :invitation-admin (:invitation-admin val)))) (defn filter-chats [db] (fn [val] diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 1921c73844..a50cff4271 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -31,6 +31,11 @@ emoji-reaction-sad (:sad resources/reactions) emoji-reaction-angry (:angry resources/reactions)}) +(def invitation-state-unknown 0) +(def invitation-state-requested 1) +(def invitation-state-rejected 2) +(def invitation-state-approved 3) + (def message-type-one-to-one 1) (def message-type-public-group 2) (def message-type-private-group 3) diff --git a/src/status_im/data_store/chats.cljs b/src/status_im/data_store/chats.cljs index 2950950823..b1c67fe519 100644 --- a/src/status_im/data_store/chats.cljs +++ b/src/status_im/data_store/chats.cljs @@ -89,7 +89,8 @@ :unviewedMessagesCount :unviewed-messages-count :lastMessage :last-message :active :is-active - :lastClockValue :last-clock-value}) + :lastClockValue :last-clock-value + :invitationAdmin :invitation-admin}) (update :last-message #(when % (messages/<-rpc %))) (dissoc :chatType :members))) diff --git a/src/status_im/data_store/invitations.cljs b/src/status_im/data_store/invitations.cljs new file mode 100644 index 0000000000..b963e47427 --- /dev/null +++ b/src/status_im/data_store/invitations.cljs @@ -0,0 +1,8 @@ +(ns status-im.data-store.invitations + (:require clojure.set)) + +(defn <-rpc [message] + (-> message + (clojure.set/rename-keys {:chatId :chat-id + :introductionMessage :introduction-message + :messageType :message-type}))) \ No newline at end of file diff --git a/src/status_im/ethereum/json_rpc.cljs b/src/status_im/ethereum/json_rpc.cljs index cee2d309b7..afba343ac0 100644 --- a/src/status_im/ethereum/json_rpc.cljs +++ b/src/status_im/ethereum/json_rpc.cljs @@ -51,6 +51,7 @@ "shhext_leaveGroupChat" {} "shhext_changeGroupChatName" {} "shhext_createGroupChatWithMembers" {} + "shhext_createGroupChatFromInvitation" {} "shhext_reSendChatMessage" {} "shhext_getOurInstallations" {} "shhext_setInstallationMetadata" {} @@ -89,6 +90,9 @@ "shhext_sendTransaction" {} "shhext_acceptRequestTransaction" {} "shhext_signMessageWithChatKey" {} + "shhext_sendGroupChatInvitationRequest" {} + "shhext_sendGroupChatInvitationRejection" {} + "shhext_getGroupChatInvitations" {} "wakuext_post" {} "wakuext_startMessenger" {} "wakuext_sendPairInstallation" {} @@ -106,6 +110,7 @@ "wakuext_leaveGroupChat" {} "wakuext_changeGroupChatName" {} "wakuext_createGroupChatWithMembers" {} + "wakuext_createGroupChatFromInvitation" {} "wakuext_reSendChatMessage" {} "wakuext_getOurInstallations" {} "wakuext_setInstallationMetadata" {} @@ -144,6 +149,9 @@ "wakuext_sendTransaction" {} "wakuext_acceptRequestTransaction" {} "wakuext_signMessageWithChatKey" {} + "wakuext_sendGroupChatInvitationRequest" {} + "wakuext_sendGroupChatInvitationRejection" {} + "wakuext_getGroupChatInvitations" {} "status_chats" {} "wallet_getTransfers" {} "wallet_getTokensBalances" {} diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index fe8fdb8d65..dd23835685 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -67,7 +67,8 @@ status-im.http.core status-im.ui.screens.profile.events status-im.chat.models.images - status-im.ui.screens.privacy-and-security-settings.events)) + status-im.ui.screens.privacy-and-security-settings.events + [status-im.data-store.invitations :as data-store.invitations])) ;; init module (handlers/register-handler-fx @@ -776,14 +777,16 @@ (fn [_ [_ err]] (log/error :send-status-message-error err))) -(fx/defn handle-update [cofx {:keys [chats messages emojiReactions] :as response}] +(fx/defn handle-update [cofx {:keys [chats messages emojiReactions invitations]}] (let [chats (map data-store.chats/<-rpc chats) messages (map data-store.messages/<-rpc messages) message-fxs (map chat.message/receive-one messages) emoji-reactions (map data-store.reactions/<-rpc emojiReactions) emoji-react-fxs (map chat.reactions/receive-one emoji-reactions) + invitations-fxs [(group-chats/handle-invitations + (map data-store.invitations/<-rpc invitations))] chat-fxs (map #(chat/ensure-chat (dissoc % :unviewed-messages-count)) chats)] - (apply fx/merge cofx (concat chat-fxs message-fxs emoji-react-fxs)))) + (apply fx/merge cofx (concat chat-fxs message-fxs emoji-react-fxs invitations-fxs)))) (handlers/register-handler-fx :transport/message-sent @@ -805,6 +808,11 @@ [cofx response] (handle-update cofx response)) +(fx/defn invitation-sent + {:events [:transport/invitation-sent]} + [cofx response] + (handle-update cofx response)) + (handlers/register-handler-fx :transport.callback/node-info-fetched (fn [cofx [_ node-info]] diff --git a/src/status_im/group_chats/core.cljs b/src/status_im/group_chats/core.cljs index 6bd89e3cda..e8828ba854 100644 --- a/src/status_im/group_chats/core.cljs +++ b/src/status_im/group_chats/core.cljs @@ -12,7 +12,8 @@ [status-im.transport.filters.core :as transport.filters] [status-im.navigation :as navigation] [status-im.utils.fx :as fx] - [status-im.waku.core :as waku])) + [status-im.waku.core :as waku] + [status-im.constants :as constants])) (fx/defn remove-member "Format group update message and sign membership" @@ -71,6 +72,14 @@ :params [nil group-name (into [] selected-contacts)] :on-success #(re-frame/dispatch [::chat-updated %])}]})) +(fx/defn create-from-link + [cofx {:keys [chat-id invitation-admin chat-name]}] + (if (get-in cofx [:db :chats chat-id :is-active]) + (models.chat/navigate-to-chat cofx chat-id) + {::json-rpc/call [{:method (json-rpc/call-ext-method (waku/enabled? cofx) "createGroupChatFromInvitation") + :params [chat-name chat-id invitation-admin] + :on-success #(re-frame/dispatch [::chat-updated %])}]})) + (fx/defn make-admin [{:keys [db] :as cofx} chat-id member] {::json-rpc/call [{:method (json-rpc/call-ext-method (waku/enabled? cofx) "addAdminsToGroupChat") @@ -84,6 +93,15 @@ :params [nil current-chat-id selected-participants] :on-success #(re-frame/dispatch [::chat-updated %])}]}) +(fx/defn add-members-from-invitation + "Add members to a group chat" + {:events [:group-chats.ui/add-members-from-invitation]} + [{{:keys [current-chat-id] :as db} :db :as cofx} id participant] + {:db (assoc-in db [:group-chat/invitations id :state] constants/invitation-state-approved) + ::json-rpc/call [{:method (json-rpc/call-ext-method (waku/enabled? cofx) "addMembersToGroupChat") + :params [nil current-chat-id [participant]] + :on-success #(re-frame/dispatch [::chat-updated %])}]}) + (fx/defn leave "Leave chat" {:events [:group-chats.ui/leave-chat-confirmed]} @@ -92,6 +110,16 @@ :params [nil chat-id true] :on-success #(re-frame/dispatch [::chat-updated %])}]}) +(fx/defn remove + "Remove chat" + {:events [:group-chats.ui/remove-chat-confirmed]} + [cofx chat-id] + (fx/merge cofx + (models.chat/deactivate-chat chat-id) + (models.chat/upsert-chat {:chat-id chat-id + :is-active false}) + (navigation/navigate-to-cofx :home {}))) + (defn- valid-name? [name] (spec/valid? :profile/name name)) @@ -104,3 +132,39 @@ ::json-rpc/call [{:method (json-rpc/call-ext-method (waku/enabled? cofx) "changeGroupChatName") :params [nil chat-id new-name] :on-success #(re-frame/dispatch [::chat-updated %])}]})) + +(fx/defn membership-retry + {:events [:group-chats.ui/membership-retry]} + [{{:keys [current-chat-id] :as db} :db}] + {:db (assoc-in db [:chat/memberships current-chat-id :retry?] true)}) + +(fx/defn membership-message + {:events [:group-chats.ui/update-membership-message]} + [{{:keys [current-chat-id] :as db} :db} message] + {:db (assoc-in db [:chat/memberships current-chat-id :message] message)}) + +(fx/defn send-group-chat-membership-request + "Send group chat membership request" + {:events [:send-group-chat-membership-request]} + [{{:keys [current-chat-id chats] :as db} :db :as cofx}] + (let [{:keys [invitation-admin]} (get chats current-chat-id) + message (get-in db [:chat/memberships current-chat-id :message])] + {:db (assoc-in db [:chat/memberships current-chat-id] nil) + ::json-rpc/call [{:method (json-rpc/call-ext-method (waku/enabled? cofx) "sendGroupChatInvitationRequest") + :params [nil current-chat-id invitation-admin message] + :on-success #(re-frame/dispatch [:transport/invitation-sent %])}]})) + +(fx/defn send-group-chat-membership-rejection + "Send group chat membership rejection" + {:events [:send-group-chat-membership-rejection]} + [cofx invitation-id] + {::json-rpc/call [{:method (json-rpc/call-ext-method (waku/enabled? cofx) "sendGroupChatInvitationRejection") + :params [nil invitation-id] + :on-success #(re-frame/dispatch [:transport/invitation-sent %])}]}) + +(fx/defn handle-invitations + [{db :db} invitations] + {:db (update db :group-chat/invitations #(reduce (fn [acc {:keys [id] :as inv}] + (assoc acc id inv)) + % + invitations))}) \ No newline at end of file diff --git a/src/status_im/multiaccounts/login/core.cljs b/src/status_im/multiaccounts/login/core.cljs index 760d2f415c..5a5b6426a4 100644 --- a/src/status_im/multiaccounts/login/core.cljs +++ b/src/status_im/multiaccounts/login/core.cljs @@ -29,7 +29,9 @@ [status-im.wallet.core :as wallet] [status-im.wallet.prices :as prices] [status-im.acquisition.core :as acquisition] - [taoensso.timbre :as log])) + [taoensso.timbre :as log] + [status-im.data-store.invitations :as data-store.invitations] + [status-im.waku.core :as waku])) (re-frame/reg-fx ::login @@ -107,6 +109,14 @@ all-stored-browsers)] {:db (assoc db :browser/browsers browsers)})) +(fx/defn initialize-invitations + {:events [::initialize-invitations]} + [{:keys [db]} invitations] + {:db (assoc db :group-chat/invitations (reduce (fn [acc {:keys [id] :as inv}] + (assoc acc id (data-store.invitations/<-rpc inv))) + {} + invitations))}) + (fx/defn initialize-web3-client-version {:events [::initialize-web3-client-version]} [{:keys [db]} node-version] @@ -158,6 +168,11 @@ (fx/defn initialize-appearance [cofx] {::multiaccounts/switch-theme (get-in cofx [:db :multiaccount :appearance])}) +(fx/defn get-group-chat-invitations [cofx] + {::json-rpc/call + [{:method (json-rpc/call-ext-method (waku/enabled? cofx) "getGroupChatInvitations") + :on-success #(re-frame/dispatch [::initialize-invitations %])}]}) + (fx/defn get-settings-callback {:events [::get-settings-callback]} [{:keys [db] :as cofx} settings] @@ -190,6 +205,7 @@ (contact/initialize-contacts) (stickers/init-stickers-packs) (mobile-network/on-network-status-change) + (get-group-chat-invitations) (logging/set-log-level (:log-level multiaccount)) (multiaccounts/switch-preview-privacy-mode-flag)))) diff --git a/src/status_im/qr_scanner/core.cljs b/src/status_im/qr_scanner/core.cljs index 1c71f51ce2..bf5e02b4a9 100644 --- a/src/status_im/qr_scanner/core.cljs +++ b/src/status_im/qr_scanner/core.cljs @@ -7,7 +7,8 @@ [status-im.utils.utils :as utils] [status-im.ethereum.core :as ethereum] [status-im.ui.screens.add-new.new-chat.db :as new-chat.db] - [status-im.utils.fx :as fx])) + [status-im.utils.fx :as fx] + [status-im.group-chats.core :as group-chats])) (fx/defn scan-qr-code {:events [::scan-code]} @@ -50,6 +51,9 @@ (when (seq topic) (chat/start-public-chat cofx topic {}))) +(fx/defn handle-group-chat [cofx params] + (group-chats/create-from-link cofx params)) + (fx/defn handle-view-profile [{:keys [db] :as cofx} {:keys [public-key]}] (let [own (new-chat.db/own-public-key? db public-key)] @@ -79,6 +83,7 @@ [cofx {:keys [type] :as data}] (case type :public-chat (handle-public-chat cofx data) + :group-chat (handle-group-chat cofx data) :private-chat (handle-private-chat cofx data) :contact (handle-view-profile cofx data) :browser (handle-browse cofx data) diff --git a/src/status_im/router/core.cljs b/src/status_im/router/core.cljs index a95da9e526..7d9dc5f82d 100644 --- a/src/status_im/router/core.cljs +++ b/src/status_im/router/core.cljs @@ -10,7 +10,9 @@ [status-im.ethereum.resolver :as resolver] [status-im.ethereum.stateofus :as stateofus] [cljs.spec.alpha :as spec] - [status-im.ethereum.core :as ethereum])) + [status-im.ethereum.core :as ethereum] + [status-im.utils.db :as utils.db] + [status-im.utils.http :as http])) (def ethereum-scheme "ethereum:") @@ -27,6 +29,9 @@ (def browser-extractor {[#"(.*)" :domain] {"" :browser "/" :browser}}) +(def group-chat-extractor {[#"(.*)" :params] {"" :group-chat + "/" :group-chat}}) + (def eip-extractor {#{[:prefix "-" :address] [:address]} {#{["@" :chain-id] ""} @@ -38,13 +43,19 @@ "b/" browser-extractor "browser/" browser-extractor ["p/" :chat-id] :private-chat + "g/" group-chat-extractor ["u/" :user-id] :user ["user/" :user-id] :user ["referral/" :referrer] :referrals} ethereum-scheme eip-extractor}]) +(defn parse-query-params + [url] + (let [url (goog.Uri. url)] + (http/query->map (.getQuery url)))) + (defn match-uri [uri] - (assoc (bidi/match-route routes uri) :uri uri)) + (assoc (bidi/match-route routes uri) :uri uri :query-params (parse-query-params uri))) (defn- ens-name-parse [contact-identity] (when (string? contact-identity) @@ -87,6 +98,21 @@ {:type :public-chat :error :invalid-topic})) +(defn match-group-chat [{:strs [a a1 a2]}] + (let [[admin-pk encoded-chat-name chat-id] [a a1 a2] + chat-id-parts (when (not (string/blank? chat-id)) (string/split chat-id #"-")) + chat-name (when (not (string/blank? encoded-chat-name)) (js/decodeURI encoded-chat-name))] + (if (and (not (string/blank? chat-id)) (not (string/blank? admin-pk)) (not (string/blank? chat-name)) + (> (count chat-id-parts) 1) + (not (string/blank? (first chat-id-parts))) + (utils.db/valid-public-key? admin-pk) + (utils.db/valid-public-key? (last chat-id-parts))) + {:type :group-chat + :chat-id chat-id + :invitation-admin admin-pk + :chat-name chat-name} + {:error :invalid-group-chat-data}))) + (defn match-private-chat-async [chain {:keys [chat-id]} cb] (match-contact-async chain {:user-id chat-id} @@ -143,7 +169,7 @@ :referrer referrer}) (defn handle-uri [chain uri cb] - (let [{:keys [handler route-params]} (match-uri uri)] + (let [{:keys [handler route-params query-params]} (match-uri uri)] (log/info "[router] uri " uri " matched " handler " with " route-params) (cond (= handler :public-chat) @@ -161,6 +187,9 @@ (= handler :private-chat) (match-private-chat-async chain route-params cb) + (= handler :group-chat) + (cb (match-group-chat query-params)) + (spec/valid? :global/public-key uri) (match-contact-async chain {:user-id uri} cb) diff --git a/src/status_im/router/core_test.cljs b/src/status_im/router/core_test.cljs index 55033bfb86..5624367244 100644 --- a/src/status_im/router/core_test.cljs +++ b/src/status_im/router/core_test.cljs @@ -3,11 +3,18 @@ [cljs.test :refer [deftest are] :include-macros true])) (def public-key "0x04fbce10971e1cd7253b98c7b7e54de3729ca57ce41a2bfb0d1c4e0a26f72c4b6913c3487fa1b4bb86125770f1743fb4459da05c1cbe31d938814cfaf36e252073") +(def chat-id "59eb36e6-9d4d-4724-9d3a-8a3cdc5e8a8e-0x04f383daedc92a66add4c90d8884004ef826cba113183a0052703c8c77fed1522f88f44550498d20679af98907627059a295e43212a1cd3c1f21a157704d608c13") +(def chat-name-url "Test%20group%20chat") +(def chat-name "Test group chat") (deftest parse-uris - (are [uri expected] (= (router/match-uri uri) {:handler (first expected) - :route-params (second expected) - :uri uri}) + (are [uri expected] (= (cond-> (router/match-uri uri) + (< (count expected) 3) + (assoc :query-params nil)) + {:handler (first expected) + :route-params (second expected) + :query-params (when (= 3 (count expected)) (last expected)) + :uri uri}) "status-im://status" [:public-chat {:chat-id "status"}] @@ -17,6 +24,12 @@ "status-im://b/www.cryptokitties.co" [:browser {:domain "www.cryptokitties.c"}] + (str "status-im://g/args?a=" public-key "&a1=" chat-name-url "&a2=" chat-id) + [:group-chat {:params "arg"} {"a" public-key "a1" chat-name "a2" chat-id}] + + (str "https://join.status.im/g/args?a=" public-key "&a1=" chat-name-url "&a2=" chat-id) + [:group-chat {:params "arg"} {"a" public-key "a1" chat-name "a2" chat-id}] + "https://join.status.im/status" [:public-chat {:chat-id "status"}] "https://join.status.im/u/statuse2e" [:user {:user-id "statuse2e"}] @@ -50,3 +63,19 @@ "ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7@1/transfer?uint256=1" [:ethereum {:address "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7" :chain-id "1" :function "transfer"}])) + +(def error {:error :invalid-group-chat-data}) + +(deftest match-group-chat-query + (are [query-params expected] (= (router/match-group-chat query-params) + expected) + nil error + {} error + {"b" public-key} error + {"a" public-key "a1" chat-name} error + {"a" "0x00ceded" "a1" chat-name "a2" chat-id} error + {"a" public-key "a1" chat-name "a2" public-key} error + {"a" public-key "a1" chat-name "a2" chat-id} {:type :group-chat + :chat-id chat-id + :invitation-admin public-key + :chat-name chat-name})) \ No newline at end of file diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 67c48bb5d5..f042c27212 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -115,7 +115,9 @@ (reg-root-key-sub :group-chat-profile/profile :group-chat-profile/profile) (reg-root-key-sub :selected-participants :selected-participants) (reg-root-key-sub :chat/inputs :chat/inputs) +(reg-root-key-sub :chat/memberships :chat/memberships) (reg-root-key-sub :camera-roll-photos :camera-roll-photos) +(reg-root-key-sub :group-chat/invitations :group-chat/invitations) ;;browser (reg-root-key-sub :browsers :browser/browsers) @@ -602,6 +604,13 @@ (fn [[chat-id inputs]] (get-in inputs [chat-id :input-text]))) +(re-frame/reg-sub + :chats/current-chat-membership + :<- [:chats/current-chat-id] + :<- [:chat/memberships] + (fn [[chat-id memberships]] + (get memberships chat-id))) + (re-frame/reg-sub :chats/current-chat :<- [:chats/current-raw-chat] @@ -626,7 +635,7 @@ :<- [:chats/current-raw-chat] (fn [current-chat] (select-keys current-chat - [:public? :group-chat :chat-id :chat-name :color]))) + [:public? :group-chat :chat-id :chat-name :color :invitation-admin]))) (re-frame/reg-sub :current-chat/one-to-one-chat? @@ -816,6 +825,19 @@ {:joined? (group-chats.db/joined? my-public-key chat) :inviter-pk (group-chats.db/get-inviter-pk my-public-key chat)})) +(re-frame/reg-sub + :group-chat/invitations-by-chat-id + :<- [:group-chat/invitations] + (fn [invitations [_ chat-id]] + (filter #(= (:chat-id %) chat-id) (vals invitations)))) + +(re-frame/reg-sub + :group-chat/pending-invitations-by-chat-id + (fn [[_ chat-id] _] + [(re-frame/subscribe [:group-chat/invitations-by-chat-id chat-id])]) + (fn [[invitations]] + (filter #(= constants/invitation-state-requested (:state %)) invitations))) + (re-frame/reg-sub :chats/transaction-status ;;TODO address here for transactions diff --git a/src/status_im/transport/message/core.cljs b/src/status_im/transport/message/core.cljs index 4869452282..733906cf90 100644 --- a/src/status_im/transport/message/core.cljs +++ b/src/status_im/transport/message/core.cljs @@ -9,6 +9,8 @@ [status-im.data-store.reactions :as data-store.reactions] [status-im.data-store.contacts :as data-store.contacts] [status-im.data-store.chats :as data-store.chats] + [status-im.data-store.invitations :as data-store.invitations] + [status-im.group-chats.core :as models.group] [status-im.utils.fx :as fx] [status-im.utils.types :as types])) @@ -24,6 +26,9 @@ (fx/defn handle-reactions [cofx reactions] (models.reactions/receive-signal cofx reactions)) +(fx/defn handle-invitations [cofx invitations] + (models.group/handle-invitations cofx invitations)) + (fx/defn process-response {:events [::process]} [cofx ^js response-js] @@ -31,7 +36,8 @@ ^js contacts (.-contacts response-js) ^js installations (.-installations response-js) ^js messages (.-messages response-js) - ^js emoji-reactions (.-emojiReactions response-js)] + ^js emoji-reactions (.-emojiReactions response-js) + ^js invitations (.-invitations response-js)] (cond (seq installations) (let [installations-clj (types/js->clj installations)] @@ -68,7 +74,14 @@ (js-delete response-js "emojiReactions") (fx/merge cofx {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} - (handle-reactions (map data-store.reactions/<-rpc reactions))))))) + (handle-reactions (map data-store.reactions/<-rpc reactions)))) + + (seq invitations) + (let [invitations (types/js->clj invitations)] + (js-delete response-js "invitations") + (fx/merge cofx + {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} + (handle-invitations (map data-store.invitations/<-rpc invitations))))))) (fx/defn remove-hash [{:keys [db] :as cofx} envelope-hash] diff --git a/src/status_im/ui/screens/chat/group.cljs b/src/status_im/ui/screens/chat/group.cljs index 0952d52b74..bd8809888f 100644 --- a/src/status_im/ui/screens/chat/group.cljs +++ b/src/status_im/ui/screens/chat/group.cljs @@ -6,30 +6,69 @@ [status-im.ui.screens.chat.styles.main :as style] [status-im.i18n :as i18n] [status-im.ui.components.list-selection :as list-selection] - [status-im.ui.components.colors :as colors]) + [status-im.ui.components.colors :as colors] + [status-im.constants :as constants] + [status-im.utils.debounce :as debounce]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) (defn join-chat-button [chat-id] [quo/button {:type :secondary - :on-press #(re-frame/dispatch [:group-chats.ui/join-pressed chat-id])} + :on-press #(debounce/dispatch-and-chill [:group-chats.ui/join-pressed chat-id] 2000)} (i18n/label :t/join-group-chat)]) (defn decline-chat [chat-id] [react/touchable-highlight {:on-press - #(re-frame/dispatch [:group-chats.ui/leave-chat-confirmed chat-id])} + #(debounce/dispatch-and-chill [:group-chats.ui/leave-chat-confirmed chat-id] 2000)} [react/text {:style style/decline-chat} (i18n/label :t/group-chat-decline-invitation)]]) +(defn request-membership [{:keys [state introduction-message] :as invitation}] + (let [{:keys [message retry?]} @(re-frame/subscribe [:chats/current-chat-membership])] + [react/view {:margin-horizontal 16 :margin-top 10} + (cond + (and invitation (= constants/invitation-state-requested state) (not retry?)) + [react/view + [react/text (i18n/label :t/introduce-yourself)] + [react/text {:style {:margin-top 10 :margin-bottom 16 :min-height 66 + :padding-horizontal 16 :padding-vertical 11 + :border-color colors/gray-lighter :border-width 1 + :border-radius 8 + :color colors/gray}} + introduction-message] + [react/text {:style {:align-self :flex-end :margin-bottom 30 + :color colors/gray}} + (str (count introduction-message) "/100")]] + + (and invitation (= constants/invitation-state-rejected state) (not retry?)) + [react/view + [react/text {:style {:align-self :center :margin-bottom 30}} + (i18n/label :t/membership-declined)]] + + :else + [react/view + [react/text (i18n/label :t/introduce-yourself)] + [quo/text-input {:placeholder (i18n/label :t/message) + :on-change-text #(re-frame/dispatch [:group-chats.ui/update-membership-message %]) + :max-length 100 + :multiline true + :default-value message + :container-style {:margin-top 10 :margin-bottom 16}}] + [react/text {:style {:align-self :flex-end :margin-bottom 30}} + (str (count message) "/100")]])])) + (defview group-chat-footer - [chat-id] - (letsubs [{:keys [joined?]} [:group-chat/inviter-info chat-id]] - (when-not joined? - [react/view {:style style/group-chat-join-footer} - [react/view {:style style/group-chat-join-container} - [join-chat-button chat-id] - [decline-chat chat-id]]]))) + [chat-id invitation-admin] + (letsubs [{:keys [joined?]} [:group-chat/inviter-info chat-id] + invitations [:group-chat/invitations-by-chat-id chat-id]] + (if invitation-admin + [request-membership (first invitations)] + (when-not joined? + [react/view {:style style/group-chat-join-footer} + [react/view {:style style/group-chat-join-container} + [join-chat-button chat-id] + [decline-chat chat-id]]])))) (def group-chat-description-loading [react/view {:style (merge style/intro-header-description-container @@ -96,8 +135,13 @@ :else [created-group-chat-description chat-name]))) +(defn group-chat-membership-description [] + [react/text {:style {:text-align :center :margin-horizontal 30}} + (i18n/label :t/membership-description)]) + (defn group-chat-description-container [{:keys [public? + invitation-admin chat-id chat-name loading-messages? @@ -108,5 +152,8 @@ (and no-messages? public?) [no-messages-group-chat-description-container chat-id] + invitation-admin + [group-chat-membership-description] + (not public?) [group-chat-inviter-description-container chat-id chat-name])) diff --git a/src/status_im/ui/screens/chat/sheets.cljs b/src/status_im/ui/screens/chat/sheets.cljs index 33214a3bc2..867dc393f6 100644 --- a/src/status_im/ui/screens/chat/sheets.cljs +++ b/src/status_im/ui/screens/chat/sheets.cljs @@ -91,42 +91,49 @@ :on-press #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id])}]])) (defn group-chat-accents [] - (fn [{:keys [chat-id group-chat chat-name color]}] + (fn [{:keys [chat-id group-chat chat-name color invitation-admin]}] (let [{:keys [joined?]} @(re-frame/subscribe [:group-chat/inviter-info chat-id])] - [react/view - [quo/list-item - {:theme :accent - :title chat-name - :subtitle (i18n/label :t/group-info) - :icon [chat-icon/chat-icon-view-chat-sheet - chat-id group-chat chat-name color] - :chevron true - :on-press #(hide-sheet-and-dispatch [:show-group-chat-profile chat-id])}] - [quo/list-item - {:theme :accent - :title (i18n/label :t/mark-all-read) - :accessibility-label :mark-all-read-button - :icon :main-icons/check - :on-press #(hide-sheet-and-dispatch [:chat.ui/mark-all-read-pressed chat-id])}] - [quo/list-item - {:theme :accent - :title (i18n/label :t/clear-history) - :accessibility-label :clear-history-button - :icon :main-icons/close - :on-press #(re-frame/dispatch [:chat.ui/clear-history-pressed chat-id])}] - [quo/list-item - {:theme :accent - :title (i18n/label :t/fetch-history) - :accessibility-label :fetch-history-button - :icon :main-icons/arrow-down - :on-press #(hide-sheet-and-dispatch [:chat.ui/fetch-history-pressed chat-id])}] - (when joined? + (if invitation-admin + [quo/list-item + {:theme :accent + :title (i18n/label :t/remove) + :accessibility-label :remove-group-chat + :icon :main-icons/delete + :on-press #(hide-sheet-and-dispatch [:group-chats.ui/remove-chat-confirmed chat-id])}] + [react/view [quo/list-item - {:theme :negative - :title (i18n/label :t/leave-chat) - :accessibility-label :leave-chat-button - :icon :main-icons/arrow-left - :on-press #(re-frame/dispatch [:group-chats.ui/leave-chat-pressed chat-id])}])]))) + {:theme :accent + :title chat-name + :subtitle (i18n/label :t/group-info) + :icon [chat-icon/chat-icon-view-chat-sheet + chat-id group-chat chat-name color] + :chevron true + :on-press #(hide-sheet-and-dispatch [:show-group-chat-profile chat-id])}] + [quo/list-item + {:theme :accent + :title (i18n/label :t/mark-all-read) + :accessibility-label :mark-all-read-button + :icon :main-icons/check + :on-press #(hide-sheet-and-dispatch [:chat.ui/mark-all-read-pressed chat-id])}] + [quo/list-item + {:theme :accent + :title (i18n/label :t/clear-history) + :accessibility-label :clear-history-button + :icon :main-icons/close + :on-press #(re-frame/dispatch [:chat.ui/clear-history-pressed chat-id])}] + [quo/list-item + {:theme :accent + :title (i18n/label :t/fetch-history) + :accessibility-label :fetch-history-button + :icon :main-icons/arrow-down + :on-press #(hide-sheet-and-dispatch [:chat.ui/fetch-history-pressed chat-id])}] + (when joined? + [quo/list-item + {:theme :negative + :title (i18n/label :t/leave-chat) + :accessibility-label :leave-chat-button + :icon :main-icons/arrow-left + :on-press #(re-frame/dispatch [:group-chats.ui/leave-chat-pressed chat-id])}])])))) (defn actions [{:keys [public? group-chat] :as current-chat}] diff --git a/src/status_im/ui/screens/chat/toolbar_content.cljs b/src/status_im/ui/screens/chat/toolbar_content.cljs index 8a63bbc031..f8416fe25e 100644 --- a/src/status_im/ui/screens/chat/toolbar_content.cljs +++ b/src/status_im/ui/screens/chat/toolbar_content.cljs @@ -33,6 +33,7 @@ (defview toolbar-content-view [] (letsubs [{:keys [group-chat + invitation-admin color chat-id contacts @@ -51,6 +52,6 @@ [one-to-one-name chat-id]) (when-not group-chat [contact-indicator chat-id]) - (when group-chat + (when (and group-chat (not invitation-admin)) [group-last-activity {:contacts contacts :public? public?}])]])) diff --git a/src/status_im/ui/screens/chat/views.cljs b/src/status_im/ui/screens/chat/views.cljs index 7f27bf1099..94baf96f8a 100644 --- a/src/status_im/ui/screens/chat/views.cljs +++ b/src/status_im/ui/screens/chat/views.cljs @@ -28,7 +28,11 @@ [status-im.ui.components.invite.chat :as invite.chat] [status-im.ui.screens.chat.components.accessory :as accessory] [status-im.ui.screens.chat.components.input :as components] - [status-im.ui.screens.chat.message.datemark :as message-datemark])) + [status-im.ui.screens.chat.message.datemark :as message-datemark] + [status-im.ui.components.toolbar :as toolbar] + [quo.core :as quo] + [clojure.string :as string] + [status-im.constants :as constants])) (defn topbar [] (let [current-chat @(re-frame/subscribe [:current-chat/metadata])] @@ -43,6 +47,19 @@ [sheets/actions current-chat]) :height 256}])}]}])) +(defn invitation-requests [chat-id admins] + (let [current-pk @(re-frame/subscribe [:multiaccount/public-key]) + admin? (get admins current-pk)] + (when admin? + (let [invitations @(re-frame/subscribe [:group-chat/pending-invitations-by-chat-id chat-id])] + (when (seq invitations) + [react/touchable-highlight + {:on-press #(re-frame/dispatch [:navigate-to :group-chat-invite]) + :accessibility-label :invitation-requests-button} + [react/view {:style (style/add-contact)} + [react/text {:style style/add-contact-text} + (i18n/label :t/group-membership-request)]]]))))) + (defn add-contact-bar [public-key] (let [added? @(re-frame/subscribe [:contacts/contact-added? public-key])] (when-not added? @@ -58,6 +75,7 @@ (defn chat-intro [{:keys [chat-id chat-name group-chat + invitation-admin contact-name public? color @@ -78,6 +96,7 @@ ;; Description section (if group-chat [chat.group/group-chat-description-container {:chat-id chat-id + :invitation-admin invitation-admin :loading-messages? loading-messages? :chat-name chat-name :public? public? @@ -95,7 +114,7 @@ (chat-intro (assoc opts :contact-name (first contact-names))))) (defn chat-intro-header-container - [{:keys [group-chat + [{:keys [group-chat invitation-admin might-have-join-time-messages? color chat-id chat-name public?]} @@ -108,6 +127,7 @@ (let [opts {:chat-id chat-id :group-chat group-chat + :invitation-admin invitation-admin :chat-name chat-name :public? public? :color color @@ -136,7 +156,7 @@ (defn messages-view [{:keys [chat bottom-space pan-responder space-keeper]}] - (let [{:keys [group-chat chat-id public?]} chat + (let [{:keys [group-chat chat-id public? invitation-admin]} chat messages @(re-frame/subscribe [:chats/current-chat-messages-stream]) no-messages? @(re-frame/subscribe [:chats/current-chat-no-messages?]) @@ -147,7 +167,7 @@ {:key-fn #(or (:message-id %) (:value %)) :ref #(reset! messages-list-ref %) :header (when (and group-chat (not public?)) - [chat.group/group-chat-footer chat-id]) + [chat.group/group-chat-footer chat-id invitation-admin]) :footer [:<> [chat-intro-header-container chat no-messages?] (when (and (not group-chat) (not public?)) @@ -188,6 +208,41 @@ [audio-message/audio-message-view] nil)) +(defn invitation-bar [chat-id] + (let [{:keys [state chat-id] :as invitation} + (first @(re-frame/subscribe [:group-chat/invitations-by-chat-id chat-id])) + {:keys [retry? message]} @(re-frame/subscribe [:chats/current-chat-membership])] + [react/view {:margin-horizontal 16 :margin-top 10} + (cond + (and invitation (= constants/invitation-state-requested state) (not retry?)) + [toolbar/toolbar {:show-border? true + :center + [quo/button + {:type :secondary + :disabled true} + (i18n/label :t/request-pending)]}] + + (and invitation (= constants/invitation-state-rejected state) (not retry?)) + [toolbar/toolbar {:show-border? true + :right + [quo/button + {:type :secondary + :on-press #(re-frame/dispatch [:group-chats.ui/membership-retry])} + (i18n/label :t/mailserver-retry)] + :left + [quo/button + {:type :secondary + :on-press #(re-frame/dispatch [:group-chats.ui/remove-chat-confirmed chat-id])} + (i18n/label :t/remove-group)]}] + :else + [toolbar/toolbar {:show-border? true + :center + [quo/button + {:type :secondary + :disabled (string/blank? message) + :on-press #(re-frame/dispatch [:send-group-chat-membership-request])} + (i18n/label :t/request-membership)]}])])) + (defn chat [] (let [bottom-space (reagent/atom 0) panel-space (reagent/atom 0) @@ -220,18 +275,23 @@ (when panel (js/setTimeout #(react/dismiss-keyboard!) 100)))] (fn [] - (let [{:keys [chat-id show-input? group-chat] :as current-chat} + (let [{:keys [chat-id show-input? group-chat admins invitation-admin] :as current-chat} @(re-frame/subscribe [:chats/current-chat])] [react/view {:style {:flex 1}} [connectivity/connectivity [topbar] [react/view {:style {:flex 1}} - (when-not group-chat + (if group-chat + [invitation-requests chat-id admins] [add-contact-bar chat-id]) [messages-view {:chat current-chat :bottom-space (max @bottom-space @panel-space) :pan-responder pan-responder :space-keeper space-keeper}]]] + (when (and group-chat invitation-admin) + [accessory/view {:y position-y + :on-update-inset on-update} + [invitation-bar chat-id]]) (when show-input? [accessory/view {:y position-y :pan-state pan-state diff --git a/src/status_im/ui/screens/profile/group_chat/views.cljs b/src/status_im/ui/screens/profile/group_chat/views.cljs index 8999757124..fc3f9aedc3 100644 --- a/src/status_im/ui/screens/profile/group_chat/views.cljs +++ b/src/status_im/ui/screens/profile/group_chat/views.cljs @@ -11,7 +11,17 @@ [status-im.ui.screens.chat.sheets :as chat.sheets] [status-im.ui.screens.profile.components.styles :as - profile.components.styles]) + profile.components.styles] + [status-im.ui.components.topbar :as topbar] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.copyable-text :as copyable-text] + [status-im.ui.components.list-selection :as list-selection] + [status-im.utils.universal-links.core :as universal-links] + [status-im.ui.components.common.common :as components.common] + [status-im.ui.screens.chat.message.message :as message] + [status-im.ui.screens.chat.photos :as photos] + [status-im.ui.screens.chat.utils :as chat.utils] + [status-im.utils.debounce :as debounce]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) (defn member-sheet [chat-id member us-admin?] @@ -87,6 +97,84 @@ :on-press #(re-frame/dispatch [:navigate-to :add-participants-toggle-list])}]) [chat-group-members-view chat-id admin? current-pk]]) +(defn hide-sheet-and-dispatch [event] + (re-frame/dispatch [:bottom-sheet/hide]) + (debounce/dispatch-and-chill event 2000)) + +(defn invitation-sheet [{:keys [introduction-message id]} contact] + (let [members @(re-frame/subscribe [:contacts/current-chat-contacts]) + allow-adding-members? (< (count members) constants/max-group-chat-participants)] + [react/view + (let [message {:content {:parsed-text + [{:type "paragraph" + :children [{:literal introduction-message}]}]} + :content-type constants/content-type-text}] + [react/view {:margin-bottom 8 :margin-right 16} + [react/view {:padding-left 72} + (chat.utils/format-author (multiaccounts/displayed-name contact))] + [react/view {:flex-direction :row :align-items :flex-end} + [react/view {:padding-left 16 :padding-top 4} + [photos/photo (multiaccounts/displayed-photo contact) {:size 36}]] + [message/->message message {:on-long-press identity}]]]) + [quo/list-item + {:theme :accent + :disabled (not allow-adding-members?) + :title (i18n/label :t/accept) + :subtitle (when-not allow-adding-members? (i18n/label :t/members-limit-reached)) + :accessibility-label :fetch-history-button + :icon :main-icons/checkmark-circle + :on-press #(hide-sheet-and-dispatch + [:group-chats.ui/add-members-from-invitation id (:public-key contact)])}] + [quo/list-item + {:theme :negative + :title (i18n/label :t/decline) + :accessibility-label :delete-chat-button + :icon :main-icons/cancel + :on-press #(hide-sheet-and-dispatch [:send-group-chat-membership-rejection id])}]])) + +(defn contacts-list-item [{:keys [from] :as invitation}] + (let [contact (or @(re-frame/subscribe [:contacts/contact-by-identity from]) {:public-key from})] + [quo/list-item + {:title (multiaccounts/displayed-name contact) + :icon [chat-icon/contact-icon-contacts-tab + (multiaccounts/displayed-photo contact)] + :on-press #(re-frame/dispatch [:bottom-sheet/show-sheet + {:content (fn [] + [invitation-sheet invitation contact])}])}])) + +(defview group-chat-invite [] + (letsubs [{:keys [chat-id chat-name]} [:chats/current-chat] + current-pk [:multiaccount/public-key]] + (let [invite-link (universal-links/generate-link + :group-chat + :external + (str "args?a=" current-pk "&a1=" (js/encodeURI chat-name) "&a2=" chat-id)) + invitations @(re-frame/subscribe [:group-chat/pending-invitations-by-chat-id chat-id])] + [react/view {:flex 1} + [topbar/topbar {:title (i18n/label :t/group-invite) + :accessories [{:icon :main-icons/share + :handler #(list-selection/open-share {:message invite-link})}]}] + [react/scroll-view {:flex 1} + [react/view {:margin-top 26} + [react/view {:padding-horizontal 16} + [react/text {:style {:color colors/gray}} (i18n/label :t/group-invite-link)] + [copyable-text/copyable-text-view + {:copied-text invite-link} + [react/view {:border-width 1 :border-color colors/gray-lighter + :justify-content :center :margin-top 10 + :border-radius 8 :padding-horizontal 16 :padding-vertical 11} + [react/text invite-link]]] + [react/text {:style {:color colors/gray :margin-top 22}} + (i18n/label :t/pending-invitations)]] + (if (seq invitations) + [list/flat-list + {:data invitations + :key-fn :id + :render-fn contacts-list-item}] + [react/text {:style {:color colors/gray :margin-top 28 :text-align :center + :padding-horizontal 16}} + (i18n/label :t/empty-pending-invitations-descr)])]]]))) + (defview group-chat-profile [] (letsubs [{:keys [admins chat-id joined? chat-name color contacts] :as current-chat} [:chats/current-chat] members [:contacts/current-chat-contacts] @@ -111,6 +199,18 @@ :subtitle (i18n/label :t/members-count {:count (count contacts)}) :subtitle-icon :icons/tiny-group})} [react/view profile.components.styles/profile-form + (when admin? + [quo/list-item + {:chevron true + :title (i18n/label :t/group-invite) + :accessibility-label :invite-chat-button + :icon :main-icons/share + :accessory (let [invitations + (count @(re-frame/subscribe + [:group-chat/pending-invitations-by-chat-id chat-id]))] + (when (pos? invitations) + [components.common/counter {:size 22} invitations])) + :on-press #(re-frame/dispatch [:navigate-to :group-chat-invite])}]) (when joined? [quo/list-item {:theme :negative diff --git a/src/status_im/ui/screens/routing/chat_stack.cljs b/src/status_im/ui/screens/routing/chat_stack.cljs index 0f8ee81a4f..1245fbbd5d 100644 --- a/src/status_im/ui/screens/routing/chat_stack.cljs +++ b/src/status_im/ui/screens/routing/chat_stack.cljs @@ -27,6 +27,8 @@ {:name :group-chat-profile :insets {:top false} :component profile.group-chat/group-chat-profile} + {:name :group-chat-invite + :component profile.group-chat/group-chat-invite} {:name :stickers :component stickers/packs} {:name :stickers-pack diff --git a/src/status_im/utils/http.cljs b/src/status_im/utils/http.cljs index 6d7ec6966c..38fde2dbd4 100644 --- a/src/status_im/utils/http.cljs +++ b/src/status_im/utils/http.cljs @@ -133,3 +133,23 @@ (defn url-sanitized? [uri] (not (nil? (re-find #"^(https:)([/|.|\w|\s|-])*\.(?:jpg|svg|png)$" uri)))) + +(defn- split-param [param] + (-> + (string/split param #"=") + (concat (repeat "")) + (->> + (take 2)))) + +(defn- url-decode + [string] + (some-> string str (string/replace #"\+" "%20") (js/decodeURIComponent))) + +(defn query->map + [qstr] + (when-not (string/blank? qstr) + (some->> (string/split qstr #"&") + seq + (mapcat split-param) + (map url-decode) + (apply hash-map)))) diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs index 6e146a6f59..7854b08a6e 100644 --- a/src/status_im/utils/universal_links/core.cljs +++ b/src/status_im/utils/universal_links/core.cljs @@ -13,7 +13,8 @@ [status-im.utils.fx :as fx] [taoensso.timbre :as log] [status-im.acquisition.core :as acquisition] - [status-im.wallet.choose-recipient.core :as choose-recipient])) + [status-im.wallet.choose-recipient.core :as choose-recipient] + [status-im.group-chats.core :as group-chats])) ;; TODO(yenda) investigate why `handle-universal-link` event is ;; dispatched 7 times for the same link @@ -22,10 +23,11 @@ (def domains {:external "https://join.status.im" :internal "status-im:/"}) -(def links {:public-chat "%s/%s" +(def links {:public-chat "%s/%s" :private-chat "%s/p/%s" - :user "%s/u/%s" - :browse "%s/b/%s"}) + :group-chat "%s/g/%s" + :user "%s/u/%s" + :browse "%s/b/%s"}) (defn generate-link [link-type domain-type param] (gstring/format (get links link-type) @@ -44,6 +46,10 @@ (log/info "universal-links: handling browse" url) {:browser/show-browser-selection url}) +(fx/defn handle-group-chat [cofx params] + (log/info "universal-links: handling group" params) + (group-chats/create-from-link cofx params)) + (fx/defn handle-private-chat [{:keys [db] :as cofx} {:keys [chat-id]}] (log/info "universal-links: handling private chat" chat-id) (when chat-id @@ -93,6 +99,7 @@ {:events [::match-value]} [cofx url {:keys [type] :as data}] (case type + :group-chat (handle-group-chat cofx data) :public-chat (handle-public-chat cofx data) :private-chat (handle-private-chat cofx data) :contact (handle-view-profile cofx data) diff --git a/status-go-version.json b/status-go-version.json index 6d195bdab4..747cef9333 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -2,7 +2,7 @@ "_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh ' instead", "owner": "status-im", "repo": "status-go", - "version": "v0.56.9", - "commit-sha1": "435eacecb587571887ef547d78f82d67f026db28", - "src-sha256": "0nk4fn7avj5yqsbvc3yjb2hdfhswxsvyvmxcgf0g3rr9rdhm7m19" + "version": "0.60.0", + "commit-sha1": "3b748a2e467fe0850b2630947a9dadfe180f35fb", + "src-sha256": "1fvrh62480xfjcaagvwl7n7njhavb6plw48rv9w5vy9ykqj089bx" } diff --git a/translations/en.json b/translations/en.json index d2a6d7af97..4d6b515330 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1230,5 +1230,18 @@ "update-to-see-sticker": "Update to latest version to see a nice sticker here!", "nickname": "Nickname", "add-nickname": "Add a nickname (optional)", - "nickname-description": "Nicknames help you identify others in Status.\nOnly you can see the nicknames you’ve added" + "nickname-description": "Nicknames help you identify others in Status.\nOnly you can see the nicknames you’ve added", + "accept": "Accept", + "group-invite": "Group invite", + "group-invite-link": "Group invite link", + "pending-invitations": "Pending membership requests", + "empty-pending-invitations-descr": "People who wish to join the group\nvia an invite link will appear here", + "introduce-yourself": "Introduce yourself with a brief message", + "request-pending": "Request pending…", + "membership-declined": "Membership request was declined", + "remove-group": "Remove group", + "request-membership": "Request membership", + "membership-description": "Group membership requires you to be accepted by the group admin", + "group-membership-request": "Group membership request", + "members-limit-reached": "Members limit reached" }