[#10179] Create invite link for group chat

Signed-off-by: andrey <motor4ik@gmail.com>
This commit is contained in:
andrey 2020-07-07 12:20:48 +02:00
parent 4304a5dd97
commit 89ad4f59d0
No known key found for this signature in database
GPG Key ID: 89B67245FD2F0272
24 changed files with 550 additions and 103 deletions

View File

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

View File

@ -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]

View File

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

View File

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

View File

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

View File

@ -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" {}

View File

@ -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]]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -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]

View File

@ -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]))

View File

@ -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}]

View File

@ -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?}])]]))

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' 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"
}

View File

@ -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 youve added"
"nickname-description": "Nicknames help you identify others in Status.\nOnly you can see the nicknames youve 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"
}