diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index a567d2e9bb..ae64682f7c 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -19,15 +19,27 @@ [status-im.utils.priority-map :refer [empty-message-map]] [status-im.utils.utils :as utils])) -(defn multi-user-chat? [cofx chat-id] - (get-in cofx [:db :chats chat-id :group-chat])) +(defn- get-chat [cofx chat-id] + (get-in cofx [:db :chats chat-id])) -(defn group-chat? [cofx chat-id] - (and (multi-user-chat? cofx chat-id) - (not (get-in cofx [:db :chats chat-id :public?])))) +(defn multi-user-chat? + ([chat] + (:group-chat chat)) + ([cofx chat-id] + (multi-user-chat? (get-chat cofx chat-id)))) -(defn public-chat? [cofx chat-id] - (get-in cofx [:db :chats chat-id :public?])) +(defn public-chat? + ([chat] + (:public? chat)) + ([cofx chat-id] + (public-chat? (get-chat cofx chat-id)))) + +(defn group-chat? + ([chat] + (and (multi-user-chat? chat) + (not (public-chat? chat)))) + ([cofx chat-id] + (group-chat? (get-chat cofx chat-id)))) (defn set-chat-ui-props "Updates ui-props in active chat by merging provided kvs into them" diff --git a/src/status_im/chat/models/message.cljs b/src/status_im/chat/models/message.cljs index 6ef57b71a3..3812064057 100644 --- a/src/status_im/chat/models/message.cljs +++ b/src/status_im/chat/models/message.cljs @@ -252,7 +252,7 @@ (cond (and (= :group-user-message message-type) (and (get-in cofx [:db :chats chat-id :contacts from]) - (get-in cofx [:db :chats chat-id :contacts (accounts.db/current-public-key cofx)]))) chat-id + (get-in cofx [:db :chats chat-id :members-joined (accounts.db/current-public-key cofx)]))) chat-id (and (= :public-group-user-message message-type) (get-in cofx [:db :chats chat-id :public?])) chat-id (and (= :user-message message-type) diff --git a/src/status_im/events.cljs b/src/status_im/events.cljs index 575004143b..466682adf1 100644 --- a/src/status_im/events.cljs +++ b/src/status_im/events.cljs @@ -1254,6 +1254,11 @@ (fn [cofx [_ chat-id]] (group-chats/remove cofx chat-id))) +(handlers/register-handler-fx + :group-chats.ui/join-pressed + (fn [cofx [_ chat-id]] + (group-chats/join-chat cofx chat-id))) + (handlers/register-handler-fx :group-chats.callback/sign-success [(re-frame/inject-cofx :random-guid-generator)] diff --git a/src/status_im/group_chats/core.cljs b/src/status_im/group_chats/core.cljs index a8c281b56e..beb59ccfe2 100644 --- a/src/status_im/group_chats/core.cljs +++ b/src/status_im/group_chats/core.cljs @@ -84,6 +84,8 @@ "name-changed" (and (admins from) (not (string/blank? (:name new-event)))) "members-added" (admins from) + "member-joined" (and (contacts member) + (= from member)) "admins-added" (and (admins from) (clojure.set/subset? members contacts)) "member-removed" (or @@ -168,6 +170,11 @@ :clock-value (utils.clocks/send last-clock-value) :members members}) +(defn- member-joined-event [last-clock-value member] + {:type "member-joined" + :clock-value (utils.clocks/send last-clock-value) + :member member}) + (fx/defn create "Format group update message and sign membership" [{:keys [db random-guid-generator] :as cofx} group-name] @@ -202,6 +209,20 @@ :from my-public-key :events [remove-event]}}))) +(fx/defn join-chat + "Format group update message and sign membership" + [{:keys [db] :as cofx} chat-id] + (let [my-public-key (accounts.db/current-public-key cofx) + last-clock-value (get-last-clock-value cofx chat-id) + chat (get-in cofx [:db :chats chat-id]) + event (member-joined-event last-clock-value my-public-key)] + (when (valid-event? chat (assoc event + :from + my-public-key)) + {:group-chats/sign-membership {:chat-id chat-id + :from my-public-key + :events [event]}}))) + (fx/defn make-admin "Format group update with make admin message and sign membership" [{:keys [db] :as cofx} chat-id member] @@ -283,8 +304,9 @@ (case type "chat-created" {:name name :created-at clock-value - :admins #{from} - :contacts #{from}} + :admins #{from} + :members-joined #{from} + :contacts #{from}} "name-changed" (assoc group :name name :name-changed-by from @@ -292,12 +314,16 @@ "members-added" (as-> group $ (update $ :contacts clojure.set/union (set members)) (reduce (fn [acc member] (assoc-in acc [member :added] clock-value)) $ members)) + "member-joined" (-> group + (update :members-joined conj member) + (assoc-in [member :joined] clock-value)) "admins-added" (as-> group $ (update $ :admins clojure.set/union (set members)) (reduce (fn [acc member] (assoc-in acc [member :admin-added] clock-value)) $ members)) "member-removed" (-> group (update :contacts disj member) (update :admins disj member) + (update :members-joined disj member) (assoc-in [member :removed] clock-value)) "admin-removed" (-> group (update :admins disj member) @@ -312,6 +338,7 @@ (reduce process-event {:admins #{} + :members-joined #{} :contacts #{}}))) (defn membership-changes->system-messages [cofx @@ -320,6 +347,7 @@ chat-name creator members-added + members-joined admins-added name-changed? members-removed]}] @@ -337,6 +365,9 @@ contacts-added (map get-contact (disj members-added creator)) + contacts-joined (map + get-contact + (disj members-joined creator)) contacts-removed (map get-contact members-removed)] @@ -356,6 +387,11 @@ (i18n/label :t/group-chat-member-added {:member (:name %)}) (get-in clock-values [(:public-key %) :added])) contacts-added)) + (seq members-joined) (concat (map #(format-message + % + (i18n/label :t/group-chat-member-joined {:member (:name %)}) + (get-in clock-values [(:public-key %) :joined])) + contacts-joined)) (seq admins-added) (concat (map #(format-message % (i18n/label :t/group-chat-admin-added {:member (:name %)}) @@ -373,6 +409,7 @@ name-changed? (and (seq previous-chat) (not= (:name previous-chat) (:name current-chat))) members-added (clojure.set/difference (:contacts current-chat) (:contacts previous-chat)) + members-joined (clojure.set/difference (:members-joined current-chat) (:members-joined previous-chat)) members-removed (clojure.set/difference (:contacts previous-chat) (:contacts current-chat)) admins-added (clojure.set/difference (:admins current-chat) (:admins previous-chat)) membership-changes (cond-> {:chat-id chat-id @@ -380,12 +417,14 @@ :chat-name (:name current-chat) :admins-added admins-added :members-added members-added + :members-joined members-joined :members-removed members-removed} (nil? previous-chat) (assoc :creator (extract-creator current-chat)))] (when (or name-changed? (seq admins-added) (seq members-added) + (seq members-joined) (seq members-removed)) (->> membership-changes (membership-changes->system-messages cofx clock-values) @@ -399,6 +438,21 @@ (map #(assoc % :from from) events)) all-updates)) +(defn joined? [my-public-key {:keys [members-joined]}] + (contains? members-joined my-public-key)) + +(defn invited? [my-public-key {:keys [contacts]}] + (contains? contacts my-public-key)) + +(defn get-inviter-pk [my-public-key {:keys [membership-updates] :as chat}] + (->> membership-updates + unwrap-events + (keep (fn [{:keys [from type members]}] + (when (and (= type "members-added") + (contains? members my-public-key)) + from))) + last)) + (fx/defn handle-membership-update "Upsert chat and receive message if valid" ;; Care needs to be taken here as chat-id is not coming from a whisper filter @@ -423,6 +477,7 @@ :group-chat true :membership-updates (into [] all-updates) :admins (:admins new-group) + :members-joined (:members-joined new-group) :contacts (:contacts new-group)}) (add-system-messages chat-id previous-chat new-group) #(when (and message diff --git a/src/status_im/ui/screens/chat/styles/main.cljs b/src/status_im/ui/screens/chat/styles/main.cljs index 24909f774e..167a1d5480 100644 --- a/src/status_im/ui/screens/chat/styles/main.cljs +++ b/src/status_im/ui/screens/chat/styles/main.cljs @@ -234,3 +234,6 @@ (def empty-chat-text-name {:color colors/black}) + +(def join-button + {:margin-bottom 5}) diff --git a/src/status_im/ui/screens/chat/views.cljs b/src/status_im/ui/screens/chat/views.cljs index f031ea8af3..b52797c7bd 100644 --- a/src/status_im/ui/screens/chat/views.cljs +++ b/src/status_im/ui/screens/chat/views.cljs @@ -4,6 +4,8 @@ [re-frame.core :as re-frame] [status-im.i18n :as i18n] [status-im.contact.core :as models.contact] + [status-im.chat.models :as models.chat] + [status-im.group-chats.core :as models.group-chats] [status-im.ui.screens.chat.styles.main :as style] [status-im.utils.platform :as platform] [status-im.ui.screens.chat.input.input :as input] @@ -14,6 +16,7 @@ [status-im.ui.screens.chat.message.datemark :as message-datemark] [status-im.ui.screens.chat.toolbar-content :as toolbar-content] [status-im.ui.components.animation :as animation] + [status-im.ui.components.button.view :as buttons] [status-im.ui.components.list.views :as list] [status-im.ui.components.list-selection :as list-selection] [status-im.ui.components.react :as react] @@ -114,7 +117,21 @@ [react/text {:style style/empty-chat-text-name} (:name contact)]] (i18n/label :t/empty-chat-description))]]))) -(defview messages-view [chat group-chat modal?] +(defn join-chat-button [chat-id] + [buttons/primary-button {:style style/join-button + :on-press #(re-frame/dispatch [:group-chats.ui/join-pressed chat-id])} + (i18n/label :t/join-group-chat)]) + +(defview group-chat-join-section [my-public-key {:keys [name chat-id] :as chat}] + (letsubs [contact [:contacts/contact-by-identity (models.group-chats/get-inviter-pk my-public-key chat)]] + [react/view style/empty-chat-container + [join-chat-button chat-id] + [react/text {:style style/empty-chat-text} + [react/text style/empty-chat-container-one-to-one + (i18n/label :t/join-group-chat-description {:username (:name contact) + :group-name name})]]])) + +(defview messages-view [{:keys [group-chat] :as chat} modal?] (letsubs [messages [:chats/current-chat-messages-stream] current-public-key [:account/public-key]] {:component-did-mount @@ -124,9 +141,18 @@ (re-frame/dispatch [:chat.ui/set-chat-ui-props {:messages-focused? true :input-focused? false}]))} - (if (and (empty? messages) - (:messages-initialized? chat)) + (cond + + (and (models.chat/group-chat? chat) + (models.group-chats/invited? current-public-key chat) + (not (models.group-chats/joined? current-public-key chat))) + [group-chat-join-section current-public-key chat] + + (and (empty? messages) + (:messages-initialized? chat)) [empty-chat-container chat] + + :else [list/flat-list {:data messages :key-fn #(or (:message-id %) (:value %)) :render-fn (fn [message] @@ -139,15 +165,20 @@ :enableEmptySections true :keyboardShouldPersistTaps :handled}]))) -(defview messages-view-wrapper [group-chat modal?] +(defview messages-view-wrapper [modal?] (letsubs [chat [:chats/current-chat]] - [messages-view chat group-chat modal?])) + [messages-view chat modal?])) + +(defn show-input-container? [my-public-key current-chat] + (or (not (models.chat/group-chat? current-chat)) + (models.group-chats/joined? my-public-key current-chat))) (defview chat-root [modal?] - (letsubs [{:keys [group-chat public?]} [:chats/current-chat] - show-bottom-info? [:chats/current-chat-ui-prop :show-bottom-info?] - show-message-options? [:chats/current-chat-ui-prop :show-message-options?] - current-view [:get :view-id]] + (letsubs [{:keys [public?] :as current-chat} [:chats/current-chat] + my-public-key [:account/public-key] + show-bottom-info? [:chats/current-chat-ui-prop :show-bottom-info?] + show-message-options? [:chats/current-chat-ui-prop :show-message-options?] + current-view [:get :view-id]] ;; this scroll-view is a hack that allows us to use on-blur and on-focus on Android ;; more details here: https://github.com/facebook/react-native/issues/11071 [react/scroll-view {:scroll-enabled false @@ -160,9 +191,10 @@ [chat-toolbar public? modal?] (if (or (= :chat current-view) modal?) [messages-view-animation - [messages-view-wrapper group-chat modal?]] + [messages-view-wrapper modal?]] [react/view style/message-view-preview]) - [input/container] + (when (show-input-container? my-public-key current-chat) + [input/container]) (when show-bottom-info? [bottom-info/bottom-info-view]) (when show-message-options? diff --git a/test/cljs/status_im/test/chat/models/message.cljs b/test/cljs/status_im/test/chat/models/message.cljs index 3cd9dd7be2..0a5572ba43 100644 --- a/test/cljs/status_im/test/chat/models/message.cljs +++ b/test/cljs/status_im/test/chat/models/message.cljs @@ -66,11 +66,12 @@ (is (= :sent status))))))) (deftest receive-group-chats - (let [cofx {:db {:chats {"chat-id" {:contacts #{"present" "a"}}} + (let [cofx {:db {:chats {"chat-id" {:contacts #{"present"} + :members-joined #{"a"}}} :account/account {:public-key "a"} :current-chat-id "chat-id" :view-id :chat}} - cofx-without-member (update-in cofx [:db :chats "chat-id" :contacts] disj "a") + cofx-without-member (update-in cofx [:db :chats "chat-id" :members-joined] disj "a") valid-message {:chat-id "chat-id" :from "present" :message-type :group-user-message diff --git a/test/cljs/status_im/test/group_chats/core.cljs b/test/cljs/status_im/test/group_chats/core.cljs index 8c316af1f5..2be0415b75 100644 --- a/test/cljs/status_im/test/group_chats/core.cljs +++ b/test/cljs/status_im/test/group_chats/core.cljs @@ -146,13 +146,19 @@ {:type "members-added" :clock-value 3 :from "2" - :members ["3"]}] + :members ["3"]} + {:type "member-joined" + :clock-value 4 + :from "3" + :member "3"}] expected {:name "chat-name" :created-at 0 "2" {:added 1 :admin-added 2} - "3" {:added 3} + "3" {:added 3 + :joined 4} :admins #{"1" "2"} + :members-joined #{"1" "3"} :contacts #{"1" "2" "3"}}] (is (= expected (group-chats/build-group events))))) (testing "adds and removes" @@ -164,25 +170,31 @@ :clock-value 1 :from "1" :members ["2"]} - {:type "admins-added" - :clock-value 2 - :from "1" - :members ["2"]} - {:type "admin-removed" + {:type "member-joined" :clock-value 3 :from "2" :member "2"} - {:type "member-removed" + {:type "admins-added" :clock-value 4 + :from "1" + :members ["2"]} + {:type "admin-removed" + :clock-value 5 + :from "2" + :member "2"} + {:type "member-removed" + :clock-value 6 :from "2" :member "2"}] expected {:name "chat-name" :created-at 0 "2" {:added 1 - :admin-added 2 - :admin-removed 3 - :removed 4} + :joined 3 + :admin-added 4 + :admin-removed 5 + :removed 6} :admins #{"1"} + :members-joined #{"1"} :contacts #{"1"}}] (is (= expected (group-chats/build-group events))))) (testing "an admin removing themselves" @@ -204,6 +216,7 @@ :member "2"}] expected {:name "chat-name" :created-at 0 + :members-joined #{"1"} "2" {:added 1 :admin-added 2 :removed 3} @@ -229,6 +242,7 @@ :name "new-name"}] expected {:name "new-name" :created-at 0 + :members-joined #{"1"} :name-changed-by "2" :name-changed-at 3 "2" {:added 1 @@ -249,6 +263,10 @@ :clock-value 2 :from "1" :members ["2"]} + {:type "member-joined" ; non-invited user joining + :clock-value 2 + :from "non-invited" + :member "non-invited"} {:type "admins-added" :clock-value 3 :from "1" @@ -261,15 +279,40 @@ :clock-value 5 :from "1" :member "2"} + {:type "member-joined" + :clock-value 5 + :from "2" + :member "2"} {:type "member-removed" ; can't remove an admin from the group :clock-value 6 :from "1" - :member "2"}] + :member "2"} + {:type "members-added" + :clock-value 7 + :from "2" + :members ["4"]} + {:type "member-joined" + :clock-value 8 + :from "4" + :member "4"} + {:type "member-removed" + :clock-value 9 + :from "1" + :member "4"} + {:type "member-joined" ; join after being removed + :clock-value 10 + :from "4" + :member "4"}] expected {:name "chat-name" + :members-joined #{"1" "2"} :created-at 0 "2" {:added 2 - :admin-added 3} + :admin-added 3 + :joined 5} "3" {:added 4} + "4" {:added 7 + :joined 8 + :removed 9} :admins #{"1" "2"} :contacts #{"1" "2" "3"}}] (is (= expected (group-chats/build-group events))))) @@ -292,6 +335,7 @@ :members ["3"]}] expected {:name "chat-name" :created-at 0 + :members-joined #{"1"} "2" {:added 1 :admin-added 2} "3" {:added 3} diff --git a/translations/en.json b/translations/en.json index 5916b3ef4a..c45a32eaee 100644 --- a/translations/en.json +++ b/translations/en.json @@ -38,10 +38,13 @@ "other": "You can select {{count}} more participants" }, "no-more-participants-available": "You can't add anymore participants", + "join-group-chat-description": "{{username}} invited you to join the group {{group-name}}", + "join-group-chat": "Join chat", "group-chat-created": "*{{member}}* created the group *{{name}}*", "group-chat-admin": "Admin", "group-chat-name-changed": "*{{member}}* changed the group's name to *{{name}}*", - "group-chat-member-added": "*{{member}}* joined the group", + "group-chat-member-added": "*{{member}}* has been invited", + "group-chat-member-joined": "*{{member}}* has joined the group", "group-chat-member-removed": "*{{member}}* left the group", "group-chat-admin-added": "*{{member}}* has been made admin", "group-chat-no-contacts": "You don't have any contacts yet.\nInvite your friends to start chatting",