Add joining of group chats

Members will now have to explicitly join a group chat to start receiving
messages from it.

Messages are still sent to users who have not joined for backward
compatibility.
Group updates are unaffected.
This commit is contained in:
Andrea Maria Piana 2018-12-17 15:59:04 +01:00
parent a52f3d4d08
commit 881691fbc3
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
9 changed files with 192 additions and 37 deletions

View File

@ -19,15 +19,27 @@
[status-im.utils.priority-map :refer [empty-message-map]] [status-im.utils.priority-map :refer [empty-message-map]]
[status-im.utils.utils :as utils])) [status-im.utils.utils :as utils]))
(defn multi-user-chat? [cofx chat-id] (defn- get-chat [cofx chat-id]
(get-in cofx [:db :chats chat-id :group-chat])) (get-in cofx [:db :chats chat-id]))
(defn group-chat? [cofx chat-id] (defn multi-user-chat?
(and (multi-user-chat? cofx chat-id) ([chat]
(not (get-in cofx [:db :chats chat-id :public?])))) (:group-chat chat))
([cofx chat-id]
(multi-user-chat? (get-chat cofx chat-id))))
(defn public-chat? [cofx chat-id] (defn public-chat?
(get-in cofx [:db :chats chat-id :public?])) ([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 (defn set-chat-ui-props
"Updates ui-props in active chat by merging provided kvs into them" "Updates ui-props in active chat by merging provided kvs into them"

View File

@ -252,7 +252,7 @@
(cond (cond
(and (= :group-user-message message-type) (and (= :group-user-message message-type)
(and (get-in cofx [:db :chats chat-id :contacts from]) (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) (and (= :public-group-user-message message-type)
(get-in cofx [:db :chats chat-id :public?])) chat-id (get-in cofx [:db :chats chat-id :public?])) chat-id
(and (= :user-message message-type) (and (= :user-message message-type)

View File

@ -1254,6 +1254,11 @@
(fn [cofx [_ chat-id]] (fn [cofx [_ chat-id]]
(group-chats/remove 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 (handlers/register-handler-fx
:group-chats.callback/sign-success :group-chats.callback/sign-success
[(re-frame/inject-cofx :random-guid-generator)] [(re-frame/inject-cofx :random-guid-generator)]

View File

@ -84,6 +84,8 @@
"name-changed" (and (admins from) "name-changed" (and (admins from)
(not (string/blank? (:name new-event)))) (not (string/blank? (:name new-event))))
"members-added" (admins from) "members-added" (admins from)
"member-joined" (and (contacts member)
(= from member))
"admins-added" (and (admins from) "admins-added" (and (admins from)
(clojure.set/subset? members contacts)) (clojure.set/subset? members contacts))
"member-removed" (or "member-removed" (or
@ -168,6 +170,11 @@
:clock-value (utils.clocks/send last-clock-value) :clock-value (utils.clocks/send last-clock-value)
:members members}) :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 (fx/defn create
"Format group update message and sign membership" "Format group update message and sign membership"
[{:keys [db random-guid-generator] :as cofx} group-name] [{:keys [db random-guid-generator] :as cofx} group-name]
@ -202,6 +209,20 @@
:from my-public-key :from my-public-key
:events [remove-event]}}))) :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 (fx/defn make-admin
"Format group update with make admin message and sign membership" "Format group update with make admin message and sign membership"
[{:keys [db] :as cofx} chat-id member] [{:keys [db] :as cofx} chat-id member]
@ -284,6 +305,7 @@
"chat-created" {:name name "chat-created" {:name name
:created-at clock-value :created-at clock-value
:admins #{from} :admins #{from}
:members-joined #{from}
:contacts #{from}} :contacts #{from}}
"name-changed" (assoc group "name-changed" (assoc group
:name name :name name
@ -292,12 +314,16 @@
"members-added" (as-> group $ "members-added" (as-> group $
(update $ :contacts clojure.set/union (set members)) (update $ :contacts clojure.set/union (set members))
(reduce (fn [acc member] (assoc-in acc [member :added] clock-value)) $ 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 $ "admins-added" (as-> group $
(update $ :admins clojure.set/union (set members)) (update $ :admins clojure.set/union (set members))
(reduce (fn [acc member] (assoc-in acc [member :admin-added] clock-value)) $ members)) (reduce (fn [acc member] (assoc-in acc [member :admin-added] clock-value)) $ members))
"member-removed" (-> group "member-removed" (-> group
(update :contacts disj member) (update :contacts disj member)
(update :admins disj member) (update :admins disj member)
(update :members-joined disj member)
(assoc-in [member :removed] clock-value)) (assoc-in [member :removed] clock-value))
"admin-removed" (-> group "admin-removed" (-> group
(update :admins disj member) (update :admins disj member)
@ -312,6 +338,7 @@
(reduce (reduce
process-event process-event
{:admins #{} {:admins #{}
:members-joined #{}
:contacts #{}}))) :contacts #{}})))
(defn membership-changes->system-messages [cofx (defn membership-changes->system-messages [cofx
@ -320,6 +347,7 @@
chat-name chat-name
creator creator
members-added members-added
members-joined
admins-added admins-added
name-changed? name-changed?
members-removed]}] members-removed]}]
@ -337,6 +365,9 @@
contacts-added (map contacts-added (map
get-contact get-contact
(disj members-added creator)) (disj members-added creator))
contacts-joined (map
get-contact
(disj members-joined creator))
contacts-removed (map contacts-removed (map
get-contact get-contact
members-removed)] members-removed)]
@ -356,6 +387,11 @@
(i18n/label :t/group-chat-member-added {:member (:name %)}) (i18n/label :t/group-chat-member-added {:member (:name %)})
(get-in clock-values [(:public-key %) :added])) (get-in clock-values [(:public-key %) :added]))
contacts-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 (seq admins-added) (concat (map #(format-message
% %
(i18n/label :t/group-chat-admin-added {:member (:name %)}) (i18n/label :t/group-chat-admin-added {:member (:name %)})
@ -373,6 +409,7 @@
name-changed? (and (seq previous-chat) name-changed? (and (seq previous-chat)
(not= (:name previous-chat) (:name current-chat))) (not= (:name previous-chat) (:name current-chat)))
members-added (clojure.set/difference (:contacts current-chat) (:contacts previous-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)) members-removed (clojure.set/difference (:contacts previous-chat) (:contacts current-chat))
admins-added (clojure.set/difference (:admins current-chat) (:admins previous-chat)) admins-added (clojure.set/difference (:admins current-chat) (:admins previous-chat))
membership-changes (cond-> {:chat-id chat-id membership-changes (cond-> {:chat-id chat-id
@ -380,12 +417,14 @@
:chat-name (:name current-chat) :chat-name (:name current-chat)
:admins-added admins-added :admins-added admins-added
:members-added members-added :members-added members-added
:members-joined members-joined
:members-removed members-removed} :members-removed members-removed}
(nil? previous-chat) (nil? previous-chat)
(assoc :creator (extract-creator current-chat)))] (assoc :creator (extract-creator current-chat)))]
(when (or name-changed? (when (or name-changed?
(seq admins-added) (seq admins-added)
(seq members-added) (seq members-added)
(seq members-joined)
(seq members-removed)) (seq members-removed))
(->> membership-changes (->> membership-changes
(membership-changes->system-messages cofx clock-values) (membership-changes->system-messages cofx clock-values)
@ -399,6 +438,21 @@
(map #(assoc % :from from) events)) (map #(assoc % :from from) events))
all-updates)) 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 (fx/defn handle-membership-update
"Upsert chat and receive message if valid" "Upsert chat and receive message if valid"
;; Care needs to be taken here as chat-id is not coming from a whisper filter ;; Care needs to be taken here as chat-id is not coming from a whisper filter
@ -423,6 +477,7 @@
:group-chat true :group-chat true
:membership-updates (into [] all-updates) :membership-updates (into [] all-updates)
:admins (:admins new-group) :admins (:admins new-group)
:members-joined (:members-joined new-group)
:contacts (:contacts new-group)}) :contacts (:contacts new-group)})
(add-system-messages chat-id previous-chat new-group) (add-system-messages chat-id previous-chat new-group)
#(when (and message #(when (and message

View File

@ -234,3 +234,6 @@
(def empty-chat-text-name (def empty-chat-text-name
{:color colors/black}) {:color colors/black})
(def join-button
{:margin-bottom 5})

View File

@ -4,6 +4,8 @@
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[status-im.i18n :as i18n] [status-im.i18n :as i18n]
[status-im.contact.core :as models.contact] [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.ui.screens.chat.styles.main :as style]
[status-im.utils.platform :as platform] [status-im.utils.platform :as platform]
[status-im.ui.screens.chat.input.input :as input] [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.message.datemark :as message-datemark]
[status-im.ui.screens.chat.toolbar-content :as toolbar-content] [status-im.ui.screens.chat.toolbar-content :as toolbar-content]
[status-im.ui.components.animation :as animation] [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.views :as list]
[status-im.ui.components.list-selection :as list-selection] [status-im.ui.components.list-selection :as list-selection]
[status-im.ui.components.react :as react] [status-im.ui.components.react :as react]
@ -114,7 +117,21 @@
[react/text {:style style/empty-chat-text-name} (:name contact)]] [react/text {:style style/empty-chat-text-name} (:name contact)]]
(i18n/label :t/empty-chat-description))]]))) (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] (letsubs [messages [:chats/current-chat-messages-stream]
current-public-key [:account/public-key]] current-public-key [:account/public-key]]
{:component-did-mount {:component-did-mount
@ -124,9 +141,18 @@
(re-frame/dispatch [:chat.ui/set-chat-ui-props (re-frame/dispatch [:chat.ui/set-chat-ui-props
{:messages-focused? true {:messages-focused? true
:input-focused? false}]))} :input-focused? false}]))}
(if (and (empty? messages) (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)) (:messages-initialized? chat))
[empty-chat-container chat] [empty-chat-container chat]
:else
[list/flat-list {:data messages [list/flat-list {:data messages
:key-fn #(or (:message-id %) (:value %)) :key-fn #(or (:message-id %) (:value %))
:render-fn (fn [message] :render-fn (fn [message]
@ -139,12 +165,17 @@
:enableEmptySections true :enableEmptySections true
:keyboardShouldPersistTaps :handled}]))) :keyboardShouldPersistTaps :handled}])))
(defview messages-view-wrapper [group-chat modal?] (defview messages-view-wrapper [modal?]
(letsubs [chat [:chats/current-chat]] (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?] (defview chat-root [modal?]
(letsubs [{:keys [group-chat public?]} [:chats/current-chat] (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-bottom-info? [:chats/current-chat-ui-prop :show-bottom-info?]
show-message-options? [:chats/current-chat-ui-prop :show-message-options?] show-message-options? [:chats/current-chat-ui-prop :show-message-options?]
current-view [:get :view-id]] current-view [:get :view-id]]
@ -160,9 +191,10 @@
[chat-toolbar public? modal?] [chat-toolbar public? modal?]
(if (or (= :chat current-view) modal?) (if (or (= :chat current-view) modal?)
[messages-view-animation [messages-view-animation
[messages-view-wrapper group-chat modal?]] [messages-view-wrapper modal?]]
[react/view style/message-view-preview]) [react/view style/message-view-preview])
[input/container] (when (show-input-container? my-public-key current-chat)
[input/container])
(when show-bottom-info? (when show-bottom-info?
[bottom-info/bottom-info-view]) [bottom-info/bottom-info-view])
(when show-message-options? (when show-message-options?

View File

@ -66,11 +66,12 @@
(is (= :sent status))))))) (is (= :sent status)))))))
(deftest receive-group-chats (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"} :account/account {:public-key "a"}
:current-chat-id "chat-id" :current-chat-id "chat-id"
:view-id :chat}} :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" valid-message {:chat-id "chat-id"
:from "present" :from "present"
:message-type :group-user-message :message-type :group-user-message

View File

@ -146,13 +146,19 @@
{:type "members-added" {:type "members-added"
:clock-value 3 :clock-value 3
:from "2" :from "2"
:members ["3"]}] :members ["3"]}
{:type "member-joined"
:clock-value 4
:from "3"
:member "3"}]
expected {:name "chat-name" expected {:name "chat-name"
:created-at 0 :created-at 0
"2" {:added 1 "2" {:added 1
:admin-added 2} :admin-added 2}
"3" {:added 3} "3" {:added 3
:joined 4}
:admins #{"1" "2"} :admins #{"1" "2"}
:members-joined #{"1" "3"}
:contacts #{"1" "2" "3"}}] :contacts #{"1" "2" "3"}}]
(is (= expected (group-chats/build-group events))))) (is (= expected (group-chats/build-group events)))))
(testing "adds and removes" (testing "adds and removes"
@ -164,25 +170,31 @@
:clock-value 1 :clock-value 1
:from "1" :from "1"
:members ["2"]} :members ["2"]}
{:type "admins-added" {:type "member-joined"
:clock-value 2
:from "1"
:members ["2"]}
{:type "admin-removed"
:clock-value 3 :clock-value 3
:from "2" :from "2"
:member "2"} :member "2"}
{:type "member-removed" {:type "admins-added"
:clock-value 4 :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" :from "2"
:member "2"}] :member "2"}]
expected {:name "chat-name" expected {:name "chat-name"
:created-at 0 :created-at 0
"2" {:added 1 "2" {:added 1
:admin-added 2 :joined 3
:admin-removed 3 :admin-added 4
:removed 4} :admin-removed 5
:removed 6}
:admins #{"1"} :admins #{"1"}
:members-joined #{"1"}
:contacts #{"1"}}] :contacts #{"1"}}]
(is (= expected (group-chats/build-group events))))) (is (= expected (group-chats/build-group events)))))
(testing "an admin removing themselves" (testing "an admin removing themselves"
@ -204,6 +216,7 @@
:member "2"}] :member "2"}]
expected {:name "chat-name" expected {:name "chat-name"
:created-at 0 :created-at 0
:members-joined #{"1"}
"2" {:added 1 "2" {:added 1
:admin-added 2 :admin-added 2
:removed 3} :removed 3}
@ -229,6 +242,7 @@
:name "new-name"}] :name "new-name"}]
expected {:name "new-name" expected {:name "new-name"
:created-at 0 :created-at 0
:members-joined #{"1"}
:name-changed-by "2" :name-changed-by "2"
:name-changed-at 3 :name-changed-at 3
"2" {:added 1 "2" {:added 1
@ -249,6 +263,10 @@
:clock-value 2 :clock-value 2
:from "1" :from "1"
:members ["2"]} :members ["2"]}
{:type "member-joined" ; non-invited user joining
:clock-value 2
:from "non-invited"
:member "non-invited"}
{:type "admins-added" {:type "admins-added"
:clock-value 3 :clock-value 3
:from "1" :from "1"
@ -261,15 +279,40 @@
:clock-value 5 :clock-value 5
:from "1" :from "1"
:member "2"} :member "2"}
{:type "member-joined"
:clock-value 5
:from "2"
:member "2"}
{:type "member-removed" ; can't remove an admin from the group {:type "member-removed" ; can't remove an admin from the group
:clock-value 6 :clock-value 6
:from "1" :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" expected {:name "chat-name"
:members-joined #{"1" "2"}
:created-at 0 :created-at 0
"2" {:added 2 "2" {:added 2
:admin-added 3} :admin-added 3
:joined 5}
"3" {:added 4} "3" {:added 4}
"4" {:added 7
:joined 8
:removed 9}
:admins #{"1" "2"} :admins #{"1" "2"}
:contacts #{"1" "2" "3"}}] :contacts #{"1" "2" "3"}}]
(is (= expected (group-chats/build-group events))))) (is (= expected (group-chats/build-group events)))))
@ -292,6 +335,7 @@
:members ["3"]}] :members ["3"]}]
expected {:name "chat-name" expected {:name "chat-name"
:created-at 0 :created-at 0
:members-joined #{"1"}
"2" {:added 1 "2" {:added 1
:admin-added 2} :admin-added 2}
"3" {:added 3} "3" {:added 3}

View File

@ -38,10 +38,13 @@
"other": "You can select {{count}} more participants" "other": "You can select {{count}} more participants"
}, },
"no-more-participants-available": "You can't add anymore 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-created": "*{{member}}* created the group *{{name}}*",
"group-chat-admin": "Admin", "group-chat-admin": "Admin",
"group-chat-name-changed": "*{{member}}* changed the group's name to *{{name}}*", "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-member-removed": "*{{member}}* left the group",
"group-chat-admin-added": "*{{member}}* has been made admin", "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", "group-chat-no-contacts": "You don't have any contacts yet.\nInvite your friends to start chatting",