diff --git a/.env b/.env index e0408dadb2..e487b8b427 100644 --- a/.env +++ b/.env @@ -26,3 +26,5 @@ ENABLE_REFERRAL_INVITE=1 ENABLE_QUO_PREVIEW=1 MAX_IMAGES_BATCH=5 APN_TOPIC=im.status.ethereum.pr +COMMUNITIES_ENABLED=1 +COMMUNITIES_MANAGEMENT_ENABLED=1 diff --git a/.env.e2e b/.env.e2e index b82c217477..70c06b9f16 100644 --- a/.env.e2e +++ b/.env.e2e @@ -26,3 +26,5 @@ ENABLE_REFERRAL_INVITE=1 MAX_IMAGES_BATCH=5 APN_TOPIC=im.status.ethereum.pr VERIFY_TRANSACTION_CHAIN_ID=3 +COMMUNITIES_ENABLED=1 +COMMUNITIES_MANAGEMENT_ENABLED=0 diff --git a/.env.jenkins b/.env.jenkins index d512e26490..71d39bc40e 100644 --- a/.env.jenkins +++ b/.env.jenkins @@ -26,3 +26,5 @@ APN_TOPIC=im.status.ethereum.pr BLANK_PREVIEW=0 MAX_IMAGES_BATCH=5 GOOGLE_FREE=0 +COMMUNITIES_ENABLED=1 +COMMUNITIES_MANAGEMENT_ENABLED=0 diff --git a/.env.nightly b/.env.nightly index 62b08d1343..3fcb2f5da8 100644 --- a/.env.nightly +++ b/.env.nightly @@ -20,3 +20,4 @@ ENABLE_ROOT_ALERT=1 ENABLE_REFERRAL_INVITE=0 MAX_IMAGES_BATCH=5 BLANK_PREVIEW=0 +COMMUNITIES_ENABLED=1 diff --git a/resources/images/icons/communities@2x.png b/resources/images/icons/communities@2x.png new file mode 100644 index 0000000000..d4d9582d62 Binary files /dev/null and b/resources/images/icons/communities@2x.png differ diff --git a/resources/images/icons/communities@3x.png b/resources/images/icons/communities@3x.png new file mode 100644 index 0000000000..7187477a16 Binary files /dev/null and b/resources/images/icons/communities@3x.png differ diff --git a/src/status_im/chat/models.cljs b/src/status_im/chat/models.cljs index b035e50409..90aefa95cc 100644 --- a/src/status_im/chat/models.cljs +++ b/src/status_im/chat/models.cljs @@ -10,6 +10,7 @@ [status-im.i18n :as i18n] [status-im.mailserver.core :as mailserver] [status-im.ui.components.colors :as colors] + [status-im.constants :as constants] [status-im.navigation :as navigation] [status-im.utils.clocks :as utils.clocks] [status-im.utils.fx :as fx] @@ -32,6 +33,9 @@ (def one-to-one-chat? (complement multi-user-chat?)) +(defn community-chat? [{:keys [chat-type]}] + (= chat-type constants/community-chat-type)) + (defn public-chat? ([chat] (:public? chat)) @@ -94,6 +98,7 @@ {:chat-id chat-id :name (or name "") :color (rand-nth colors/chat-colors) + :chat-type constants/one-to-one-chat-type :group-chat false :is-active true :timestamp now @@ -176,6 +181,9 @@ :name topic :chat-name (str "#" topic) :group-chat true + :chat-type (if timeline? + constants/timeline-chat-type + constants/public-chat-type) :contacts #{} :public? true :might-have-join-time-messages? (get-in cofx [:db :multiaccount :use-mailservers?]) diff --git a/src/status_im/chat/models/mentions.cljs b/src/status_im/chat/models/mentions.cljs index 032efbbda1..dc8a6ca112 100644 --- a/src/status_im/chat/models/mentions.cljs +++ b/src/status_im/chat/models/mentions.cljs @@ -1,6 +1,7 @@ (ns status-im.chat.models.mentions (:require [clojure.string :as string] [re-frame.core :as re-frame] + [status-im.constants :as constants] [status-im.utils.fx :as fx] [status-im.contact.db :as contact.db] [status-im.utils.platform :as platform] @@ -172,11 +173,11 @@ (defn get-mentionable-users [{{:keys [current-chat-id] :contacts/keys [contacts] :as db} :db}] - (let [{:keys [group-chat public? users] :as chat} + (let [{:keys [chat-type users] :as chat} (get-in db [:chats current-chat-id]) chat-specific-suggestions (cond - (and group-chat (not public?)) + (= chat-type constants/private-group-chat-type) (let [{:keys [public-key]} (:multiaccount db) all-contacts (:contacts/contacts db) group-contacts diff --git a/src/status_im/chat/models/message.cljs b/src/status_im/chat/models/message.cljs index 10c2cbbb53..b86a21fc0a 100644 --- a/src/status_im/chat/models/message.cljs +++ b/src/status_im/chat/models/message.cljs @@ -114,20 +114,6 @@ (get-in db [:chats chat-id])] (>= deleted-at-clock-value clock-value))) -(defn extract-chat-id - "Validate and return a valid chat-id" - [cofx {:keys [chat-id from message-type]}] - (cond - (and (= constants/message-type-private-group message-type) - (and (get-in cofx [:db :chats chat-id :contacts from]) - (get-in cofx [:db :chats chat-id :members-joined (multiaccounts.model/current-public-key cofx)]))) chat-id - (and (= constants/message-type-public-group message-type) - (get-in cofx [:db :chats chat-id :public?])) chat-id - (and (= constants/message-type-one-to-one message-type) - (= (multiaccounts.model/current-public-key cofx) from)) chat-id - (= constants/message-type-private-group-system-message message-type) chat-id - (= constants/message-type-one-to-one message-type) from)) - (fx/defn update-unviewed-count [{:keys [db] :as cofx} {:keys [chat-id from message-type message-id new?]}] (when-not (= message-type constants/message-type-private-group-system-message) @@ -159,24 +145,22 @@ (fx/defn receive-one {:events [::receive-one]} - [{:keys [db] :as cofx} {:keys [message-id] :as message}] - (when-let [chat-id (extract-chat-id cofx message)] - (fx/merge cofx - ;;If its a profile updates we want to add this message to the timeline as well - #(when (get-in cofx [:db :chats chat-id :profile-public-key]) - {:dispatch-n [[::receive-one (assoc message :chat-id chat-model/timeline-chat-id)]]}) - #(let [message-with-chat-id (assoc message :chat-id chat-id)] - (when-not (earlier-than-deleted-at? cofx message-with-chat-id) - (if (message-loaded? cofx message-with-chat-id) - ;; If the message is already loaded, it means it's an update, that - ;; happens when a message that was missing a reply had the reply - ;; coming through, in which case we just insert the new message - {:db (assoc-in db [:messages chat-id message-id] message-with-chat-id)} - (fx/merge cofx - (add-received-message message-with-chat-id) - (update-unviewed-count message-with-chat-id) - (chat-model/join-time-messages-checked chat-id) - (check-for-incoming-tx message-with-chat-id)))))))) + [{:keys [db] :as cofx} {:keys [message-id chat-id] :as message}] + (fx/merge cofx + ;;If its a profile updates we want to add this message to the timeline as well + #(when (get-in cofx [:db :chats chat-id :profile-public-key]) + {:dispatch-n [[::receive-one (assoc message :chat-id chat-model/timeline-chat-id)]]}) + #(when-not (earlier-than-deleted-at? cofx message) + (if (message-loaded? cofx message) + ;; If the message is already loaded, it means it's an update, that + ;; happens when a message that was missing a reply had the reply + ;; coming through, in which case we just insert the new message + {:db (assoc-in db [:messages chat-id message-id] message)} + (fx/merge cofx + (add-received-message message) + (update-unviewed-count message) + (chat-model/join-time-messages-checked chat-id) + (check-for-incoming-tx message)))))) ;;TODO currently we process every message, we need to precess them by batches ;;or better move processing to status-go diff --git a/src/status_im/chat/models/message_test.cljs b/src/status_im/chat/models/message_test.cljs index d9ab26dcd8..be5116784e 100644 --- a/src/status_im/chat/models/message_test.cljs +++ b/src/status_im/chat/models/message_test.cljs @@ -3,11 +3,8 @@ [status-im.chat.models.loading :as chat-loading] [status-im.chat.models.message :as message] [status-im.chat.models.message-list :as models.message-list] - [status-im.constants :as constants] [status-im.ui.screens.chat.state :as view.state] - [status-im.utils.datetime :as time] - [status-im.utils.gfycat.core :as gfycat] - [status-im.utils.identicon :as identicon])) + [status-im.utils.datetime :as time])) (deftest add-received-message-test (with-redefs [message/add-message (constantly :added)] @@ -154,129 +151,6 @@ :clock-value 0 :chat-id "a"})))) -(deftest add-own-received-message - (let [db {:multiaccount {:public-key "me"} - :view-id :chat - :loaded-chat-id "chat-id" - :current-chat-id "chat-id" - :messages {"chat-id" {}} - :chats {"chat-id" {}}}] - (testing "a message coming from you!" - (let [actual (message/receive-one {:db db} - {:from "me" - :message-type constants/message-type-one-to-one - :timestamp 0 - :whisper-timestamp 0 - :message-id "id" - :chat-id "chat-id" - :outgoing true - :content "b" - :clock-value 1}) - message (get-in actual [:db :messages "chat-id" "id"])] - (testing "it adds the message" - (is message)))))) - -(deftest receive-group-chats - (let [cofx {:db {:chats {"chat-id" {:contacts #{"present"} - :members-joined #{"a"}}} - :multiaccount {:public-key "a"} - :loaded-chat-id "chat-id" - :current-chat-id "chat-id" - :view-id :chat}} - cofx-without-member (update-in cofx [:db :chats "chat-id" :members-joined] disj "a") - valid-message {:chat-id "chat-id" - :from "present" - :message-type constants/message-type-private-group - :message-id "1" - :clock-value 1 - :whisper-timestamp 0 - :timestamp 0} - bad-chat-id-message {:chat-id "bad-chat-id" - :from "present" - :message-type constants/message-type-private-group - :message-id "1" - :clock-value 1 - :whisper-timestamp 0 - :timestamp 0} - bad-from-message {:chat-id "chat-id" - :from "not-present" - :message-type constants/message-type-private-group - :message-id "1" - :clock-value 1 - :whisper-timestamp 0 - :timestamp 0}] - (testing "a valid message" - (is (get-in (message/receive-one cofx valid-message) [:db :messages "chat-id" "1"]))) - (testing "a message from someone not in the list of participants" - (is (not (message/receive-one cofx bad-from-message)))) - (testing "a message with non existing chat-id" - (is (not (message/receive-one cofx bad-chat-id-message)))) - (testing "a message from a delete chat" - (is (not (message/receive-one cofx-without-member valid-message)))))) - -(deftest receive-public-chats - (let [cofx {:db {:chats {"chat-id" {:public? true}} - :multiaccount {:public-key "a"} - :loaded-chat-id "chat-id" - :current-chat-id "chat-id" - :view-id :chat}} - valid-message {:chat-id "chat-id" - :from "anyone" - :message-type constants/message-type-public-group - :message-id "1" - :clock-value 1 - :whisper-timestamp 0 - :timestamp 0} - bad-chat-id-message {:chat-id "bad-chat-id" - :from "present" - :message-type constants/message-type-public-group - :message-id "1" - :clock-value 1 - :whisper-timestamp 0 - :timestamp 0}] - (testing "a valid message" - (is (get-in (message/receive-one cofx valid-message) [:db :messages "chat-id" "1"]))) - (testing "a message with non existing chat-id" - (is (not (message/receive-one cofx bad-chat-id-message)))))) - -(deftest receive-one-to-one - (with-redefs [gfycat/generate-gfy (constantly "generated") - identicon/identicon (constantly "generated")] - - (let [cofx {:db {:chats {"matching" {}} - :multiaccount {:public-key "me"} - :current-chat-id "matching" - :loaded-chat-id "matching" - :view-id :chat}} - valid-message {:chat-id "matching" - :from "matching" - :message-type constants/message-type-one-to-one - :message-id "1" - :clock-value 1 - :whisper-timestamp 0 - :timestamp 0} - own-message {:chat-id "matching" - :from "me" - :message-type constants/message-type-one-to-one - :message-id "1" - :clock-value 1 - :whisper-timestamp 0 - :timestamp 0} - - bad-chat-id-message {:chat-id "bad-chat-id" - :from "not-matching" - :message-type constants/message-type-one-to-one - :message-id "1" - :clock-value 1 - :whisper-timestamp 0 - :timestamp 0}] - (testing "a valid message" - (is (get-in (message/receive-one cofx valid-message) [:db :messages "matching" "1"]))) - (testing "our own message" - (is (get-in (message/receive-one cofx own-message) [:db :messages "matching" "1"]))) - (testing "a message with non matching chat-id" - (is (not (get-in (message/receive-one cofx bad-chat-id-message) [:db :messages "not-matching" "1"]))))))) - (deftest delete-message (with-redefs [time/day-relative (constantly "day-relative") time/timestamp->time (constantly "timestamp")] diff --git a/src/status_im/communities/core.cljs b/src/status_im/communities/core.cljs new file mode 100644 index 0000000000..7f9fb8ec3f --- /dev/null +++ b/src/status_im/communities/core.cljs @@ -0,0 +1,319 @@ +(ns status-im.communities.core + (:require + [re-frame.core :as re-frame] + [clojure.walk :as walk] + [taoensso.timbre :as log] + [status-im.utils.fx :as fx] + [status-im.constants :as constants] + [status-im.chat.models :as models.chat] + [status-im.transport.filters.core :as models.filters] + [status-im.ui.components.bottom-sheet.core :as bottom-sheet] + [status-im.data-store.chats :as data-store.chats] + [status-im.ethereum.json-rpc :as json-rpc])) + +(def featured + [{:name "Status" + :id constants/status-community-id}]) + +(def access-no-membership 1) +(def access-invitation-only 2) +(def access-on-request 3) + +(defn <-chats-rpc [chats] + (reduce-kv (fn [acc k v] + (assoc acc + (name k) + (-> v + (update :members walk/stringify-keys) + (assoc :identity {:display-name (get-in v [:identity :display_name]) + :description (get-in v [:identity :description])} + :id (name k))))) + {} + chats)) + +(defn <-rpc [{:keys [description] :as c}] + (let [identity (:identity description)] + (-> c + (update-in [:description :members] walk/stringify-keys) + (assoc-in [:description :identity] {:display-name (:display_name identity) + :description (:description identity)}) + (update-in [:description :chats] <-chats-rpc)))) + +(fx/defn handle-chats [cofx chats] + (models.chat/ensure-chats cofx chats)) + +(fx/defn handle-filters [cofx filters] + (models.filters/handle-filters cofx filters)) + +(fx/defn handle-removed-filters [cofx filters] + (models.filters/handle-filters-removed cofx (map models.filters/responses->filters filters))) + +(fx/defn handle-removed-chats [{:keys [db]} chat-ids] + {:db (reduce (fn [db chat-id] + (update db :chats dissoc chat-id)) + db + chat-ids)}) + +(fx/defn handle-community + [{:keys [db]} {:keys [id] :as community}] + {:db (assoc-in db [:communities id] (<-rpc community))}) + +(fx/defn handle-fetched + {:events [::fetched]} + [{:keys [db]} communities] + {:db (reduce (fn [db {:keys [id] :as community}] + (assoc-in db [:communities id] (<-rpc community))) + db + communities)}) + +(fx/defn handle-response [cofx response] + (fx/merge cofx + (handle-removed-chats (:removedChats response)) + (handle-chats (map #(-> % + (data-store.chats/<-rpc) + (dissoc :unviewed-messages-count)) + (:chats response))) + (handle-fetched (:communities response)) + (handle-removed-filters (:removedFilters response)) + (handle-filters (:filters response)))) + +(fx/defn left + {:events [::left]} + [cofx response] + (handle-response cofx response)) + +(fx/defn joined + {:events [::joined]} + [cofx response] + (handle-response cofx response)) + +(fx/defn export + [cofx community-id on-success] + {::json-rpc/call [{:method "wakuext_exportCommunity" + :params [community-id] + :on-success on-success + :on-error #(do + (log/error "failed to export community" community-id %) + (re-frame/dispatch [::failed-to-export %]))}]}) +(fx/defn import-community + {:events [::import]} + [cofx community-key on-success] + {::json-rpc/call [{:method "wakuext_importCommunity" + :params [community-key] + :on-success on-success + :on-error #(do + (log/error "failed to import community" %) + (re-frame/dispatch [::failed-to-import %]))}]}) + +(fx/defn join + {:events [::join]} + [cofx community-id] + {::json-rpc/call [{:method "wakuext_joinCommunity" + :params [community-id] + :on-success #(re-frame/dispatch [::joined %]) + :on-error #(do + (log/error "failed to join community" community-id %) + (re-frame/dispatch [::failed-to-join %]))}]}) + +(fx/defn leave + {:events [::leave]} + [cofx community-id] + {::json-rpc/call [{:method "wakuext_leaveCommunity" + :params [community-id] + :on-success #(re-frame/dispatch [::left %]) + :on-error #(do + (log/error "failed to leave community" community-id %) + (re-frame/dispatch [::failed-to-leave %]))}]}) + +(fx/defn fetch [_] + {::json-rpc/call [{:method "wakuext_communities" + :params [] + :on-success #(re-frame/dispatch [::fetched %]) + :on-error #(do + (log/error "failed to fetch communities" %) + (re-frame/dispatch [::failed-to-fetch %]))}]}) + +(fx/defn chat-created + {:events [::chat-created]} + [cofx community-id user-pk] + {::json-rpc/call [{:method "wakuext_sendChatMessage" + :params [{:chatId user-pk + :text "Upgrade here to see an invitation to community" + :communityId community-id + :contentType constants/content-type-community}] + :on-success + #(re-frame/dispatch [:transport/message-sent % 1]) + :on-failure #(log/error "failed to send a message" %)}]}) + +(fx/defn invite-user [cofx + community-id + user-pk + on-success-event + on-failure-event] + + (fx/merge cofx + {::json-rpc/call [{:method "wakuext_inviteUserToCommunity" + :params [community-id + user-pk] + :on-success #(re-frame/dispatch [on-success-event %]) + :on-error #(do + (log/error "failed to invite-user community" %) + (re-frame/dispatch [on-failure-event %]))}]} + (models.chat/upsert-chat {:chat-id user-pk + :active (get-in cofx [:db :chats user-pk :active])} + #(re-frame/dispatch [::chat-created community-id user-pk])))) + +(fx/defn create [{:keys [db]} + community-name + community-description + community-membership + on-success-event + on-failure-event] + (let [membership (js/parseInt community-membership) + my-public-key (get-in db [:multiaccount :public-key])] + {::json-rpc/call [{:method "wakuext_createCommunity" + :params [{:identity {:display_name community-name + :description community-description} + :members {my-public-key {}} + :permissions {:access membership}}] + :on-success #(re-frame/dispatch [on-success-event %]) + :on-error #(do + (log/error "failed to create community" %) + (re-frame/dispatch [on-failure-event %]))}]})) + +(defn create-channel [community-id + community-channel-name + community-channel-description + on-success-event + on-failure-event] + {::json-rpc/call [{:method "wakuext_createCommunityChat" + :params [community-id + {:identity {:display_name community-channel-name + :description community-channel-description} + :permissions {:access access-no-membership}}] + :on-success #(re-frame/dispatch [on-success-event %]) + :on-error #(do + (log/error "failed to create community channel" %) + (re-frame/dispatch [on-failure-event %]))}]}) + +(def no-membership-access 1) +(def invitation-only-access 2) +(def on-request-access 3) + +(defn require-membership? [permissions] + (not= no-membership-access (:access permissions))) + +(def community-id-length 68) +;; TODO: test this +(defn can-post? [{:keys [admin] :as community} pk local-chat-id] + (let [chat-id (subs local-chat-id community-id-length) + can-access-community? (or (get-in community [:description :members pk]) + (not (require-membership? (get-in community [:description :permissions]))))] + (or admin + (get-in community [:description :chats chat-id :members pk]) + (and can-access-community? + (not (require-membership? (get-in community [:description :chats chat-id :permissions]))))))) + +(fx/defn reset-community-id-input [{:keys [db]} id] + {:db (assoc db :communities/community-id-input id)}) + +(defn fetch-community-id-input [{:keys [db]}] + (:communities/community-id-input db)) + +(fx/defn import-pressed + {:events [::import-pressed]} + [cofx] + (bottom-sheet/show-bottom-sheet cofx {:view :import-community})) + +(fx/defn create-pressed + {:events [::create-pressed]} + [cofx] + (bottom-sheet/show-bottom-sheet cofx {:view :create-community})) + +(fx/defn invite-people-pressed + {:events [::invite-people-pressed]} + [cofx id] + (fx/merge cofx + (reset-community-id-input id) + (bottom-sheet/show-bottom-sheet {:view :invite-people-community}))) + +(fx/defn create-channel-pressed + {:events [::create-channel-pressed]} + [cofx id] + (fx/merge cofx + (reset-community-id-input id) + (bottom-sheet/show-bottom-sheet {:view :create-community-channel}))) + +(fx/defn community-created + {:events [::community-created]} + [cofx response] + (fx/merge cofx + (bottom-sheet/hide-bottom-sheet) + (handle-response response))) + +(fx/defn community-imported + {:events [::community-imported]} + [cofx response] + (fx/merge cofx + (bottom-sheet/hide-bottom-sheet) + (handle-response response))) + +(fx/defn people-invited + {:events [::people-invited]} + [cofx response] + (fx/merge cofx + (bottom-sheet/hide-bottom-sheet) + (handle-response response))) + +(fx/defn community-channel-created + {:events [::community-channel-created]} + [cofx response] + (fx/merge cofx + (bottom-sheet/hide-bottom-sheet) + (handle-response response))) + +(fx/defn handle-export-pressed + {:events [::export-pressed]} + [cofx community-id] + (export cofx community-id + #(re-frame/dispatch [:show-popover {:view :export-community + :community-key %}]))) + +(fx/defn import-confirmation-pressed + {:events [::import-confirmation-pressed]} + [cofx community-key] + (import-community + cofx + community-key + #(re-frame/dispatch [::community-imported %]))) + +(fx/defn create-confirmation-pressed + {:events [::create-confirmation-pressed]} + [cofx community-name community-description membership] + (create + cofx + community-name + community-description + membership + ::community-created + ::failed-to-create-community)) + +(fx/defn create-channel-confirmation-pressed + {:events [::create-channel-confirmation-pressed]} + [cofx community-channel-name community-channel-description] + (create-channel + (fetch-community-id-input cofx) + community-channel-name + community-channel-description + ::community-channel-created + ::failed-to-create-community-channel)) + +(fx/defn invite-people-confirmation-pressed + {:events [::invite-people-confirmation-pressed]} + [cofx user-pk] + (invite-user + cofx + (fetch-community-id-input cofx) + user-pk + ::people-invited + ::failed-to-invite-people)) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 9d790e5cbb..1887e88247 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -14,6 +14,7 @@ (def content-type-system-text 6) (def content-type-image 7) (def content-type-audio 8) +(def content-type-community 9) (def emoji-reaction-love 1) (def emoji-reaction-thumbs-up 2) @@ -22,6 +23,13 @@ (def emoji-reaction-sad 5) (def emoji-reaction-angry 6) +(def one-to-one-chat-type 1) +(def public-chat-type 2) +(def private-group-chat-type 3) +(def profile-chat-type 4) +(def timeline-chat-type 5) +(def community-chat-type 6) + (def reactions {emoji-reaction-love (:love resources/reactions) emoji-reaction-thumbs-up (:thumbs-up resources/reactions) emoji-reaction-thumbs-down (:thumbs-down resources/reactions) @@ -199,3 +207,5 @@ (def faq "https://status.im/faq/") (def faq-keycard (str faq "#keycard")) (def keycard-integration-link "https://status.im/keycard-integration") + +(def status-community-id "0x039b2da47552aa117a96ea8f1d4d108ba66637c7517a3c94a57b99dbb8a002eda2") diff --git a/src/status_im/data_store/chats.cljs b/src/status_im/data_store/chats.cljs index d1257de15c..b9b086d104 100644 --- a/src/status_im/data_store/chats.cljs +++ b/src/status_im/data_store/chats.cljs @@ -2,41 +2,41 @@ (:require [clojure.set :as clojure.set] [status-im.data-store.messages :as messages] [status-im.ethereum.json-rpc :as json-rpc] + [status-im.constants :as constants] [status-im.utils.fx :as fx] [taoensso.timbre :as log])) -(def one-to-one-chat-type 1) -(def public-chat-type 2) -(def private-group-chat-type 3) -(def profile-chat-type 4) -(def timeline-chat-type 5) - -(defn type->rpc [{:keys [public? group-chat profile-public-key timeline?] :as chat}] - (assoc chat :chatType (cond - profile-public-key profile-chat-type - timeline? timeline-chat-type - public? public-chat-type - group-chat private-group-chat-type - :else one-to-one-chat-type))) +(defn type->rpc [{:keys [chat-type public? group-chat profile-public-key timeline?] :as chat}] + (if chat-type + (assoc chat :chatType chat-type) + (assoc chat :chatType (cond + profile-public-key constants/profile-chat-type + timeline? constants/timeline-chat-type + public? constants/public-chat-type + group-chat constants/private-group-chat-type + :else constants/one-to-one-chat-type)))) (defn rpc->type [{:keys [chatType name] :as chat}] (cond - (or (= public-chat-type chatType) - (= profile-chat-type chatType) - (= timeline-chat-type chatType)) (assoc chat - :chat-name (str "#" name) - :public? true - :group-chat true - :timeline? (= timeline-chat-type chatType)) - (= private-group-chat-type chatType) (assoc chat - :chat-name name - :public? false - :group-chat true) + (or (= constants/public-chat-type chatType) + (= constants/profile-chat-type chatType) + (= constants/timeline-chat-type chatType)) (assoc chat + :chat-name (str "#" name) + :public? true + :group-chat true + :timeline? (= constants/timeline-chat-type chatType)) + (= constants/community-chat-type chatType) (assoc chat + :chat-name name + :group-chat true) + (= constants/private-group-chat-type chatType) (assoc chat + :chat-name name + :public? false + :group-chat true) :else (assoc chat :public? false :group-chat false))) -(defn- marshal-members [{:keys [admins contacts members-joined chatType] :as chat}] +(defn- marshal-members [{:keys [admins contacts members-joined chat-type] :as chat}] (cond-> chat - (= chatType private-group-chat-type) + (= chat-type constants/private-group-chat-type) (assoc :members (map #(hash-map :id % :admin (boolean (admins %)) :joined (boolean (members-joined %))) contacts)) @@ -45,23 +45,23 @@ (defn- unmarshal-members [{:keys [members chatType] :as chat}] (cond - (= public-chat-type chatType) (assoc chat - :contacts #{} - :admins #{} - :members-joined #{}) - (= private-group-chat-type chatType) (merge chat - (reduce (fn [acc member] - (cond-> acc - (:admin member) - (update :admins conj (:id member)) - (:joined member) - (update :members-joined conj (:id member)) - :always - (update :contacts conj (:id member)))) - {:admins #{} - :members-joined #{} - :contacts #{}} - members)) + (= constants/public-chat-type chatType) (assoc chat + :contacts #{} + :admins #{} + :members-joined #{}) + (= constants/private-group-chat-type chatType) (merge chat + (reduce (fn [acc member] + (cond-> acc + (:admin member) + (update :admins conj (:id member)) + (:joined member) + (update :members-joined conj (:id member)) + :always + (update :contacts conj (:id member)))) + {:admins #{} + :members-joined #{} + :contacts #{}} + members)) :else (assoc chat :contacts #{(:id chat)} @@ -70,20 +70,21 @@ (defn- ->rpc [chat] (-> chat - type->rpc marshal-members (update :last-message messages/->rpc) + type->rpc (clojure.set/rename-keys {:chat-id :id :membership-update-events :membershipUpdateEvents :unviewed-messages-count :unviewedMessagesCount :last-message :lastMessage + :community-id :communityId :deleted-at-clock-value :deletedAtClockValue :is-active :active :last-clock-value :lastClockValue :profile-public-key :profile}) (dissoc :public? :group-chat :messages :might-have-join-time-messages? - :loaded-unviewed-messages-ids + :loaded-unviewed-messages-ids :chat-type :contacts :admins :members-joined))) (defn <-rpc [chat] @@ -91,8 +92,10 @@ rpc->type unmarshal-members (clojure.set/rename-keys {:id :chat-id + :communityId :community-id :membershipUpdateEvents :membership-update-events :deletedAtClockValue :deleted-at-clock-value + :chatType :chat-type :unviewedMessagesCount :unviewed-messages-count :lastMessage :last-message :active :is-active @@ -100,7 +103,7 @@ :invitationAdmin :invitation-admin :profile :profile-public-key}) (update :last-message #(when % (messages/<-rpc %))) - (dissoc :chatType :members))) + (dissoc :members))) (fx/defn save-chat [cofx {:keys [chat-id] :as chat} on-success] {::json-rpc/call [{:method (json-rpc/call-ext-method "saveChat") diff --git a/src/status_im/data_store/chats_test.cljs b/src/status_im/data_store/chats_test.cljs index 2e30591083..70efced626 100644 --- a/src/status_im/data_store/chats_test.cljs +++ b/src/status_im/data_store/chats_test.cljs @@ -8,6 +8,7 @@ :color "color" :contacts #{"a" "b" "c" "d"} :last-clock-value 10 + :chat-type 3 :admins #{"a" "b"} :members-joined #{"a" "c"} :name "name" @@ -70,6 +71,7 @@ :color "color" :chat-name "name" :contacts #{"a" "b" "c" "d"} + :chat-type 3 :last-clock-value 10 :last-message nil :admins #{"a" "b"} diff --git a/src/status_im/data_store/messages.cljs b/src/status_im/data_store/messages.cljs index 16eeb38fc7..01711e6f41 100644 --- a/src/status_im/data_store/messages.cljs +++ b/src/status_im/data_store/messages.cljs @@ -11,6 +11,7 @@ :sticker (:sticker content)) :always (clojure.set/rename-keys {:chat-id :chatId + :community-id :communityId :clock-value :clock}))) (defn <-rpc [message] @@ -20,6 +21,7 @@ :commandParameters :command-parameters :messageType :message-type :localChatId :chat-id + :communityId :community-id :contentType :content-type :clock :clock-value :quotedMessage :quoted-message @@ -27,7 +29,7 @@ :audioDurationMs :audio-duration-ms :new :new?}) - (update :quoted-message clojure.set/rename-keys {:parsedText :parsed-text}) + (update :quoted-message clojure.set/rename-keys {:parsedText :parsed-text :communityId :community-id}) (update :outgoing-status keyword) (update :command-parameters clojure.set/rename-keys {:transactionHash :transaction-hash :commandState :command-state}) diff --git a/src/status_im/ethereum/json_rpc.cljs b/src/status_im/ethereum/json_rpc.cljs index 9001f1b880..b0f40cb227 100644 --- a/src/status_im/ethereum/json_rpc.cljs +++ b/src/status_im/ethereum/json_rpc.cljs @@ -115,6 +115,14 @@ "multiaccounts_getIdentityImage" {} "multiaccounts_storeIdentityImage" {} "multiaccounts_deleteIdentityImage" {} + "wakuext_createCommunity" {} + "wakuext_createCommunityChat" {} + "wakuext_inviteUserToCommunity" {} + "wakuext_joinCommunity" {} + "wakuext_leaveCommunity" {} + "wakuext_communities" {} + "wakuext_importCommunity" {} + "wakuext_exportCommunity" {} "status_chats" {} "localnotifications_switchWalletNotifications" {} "localnotifications_notificationPreferences" {} diff --git a/src/status_im/keycard/test_menu.cljs b/src/status_im/keycard/test_menu.cljs index e2d98161ec..22b081404f 100644 --- a/src/status_im/keycard/test_menu.cljs +++ b/src/status_im/keycard/test_menu.cljs @@ -5,7 +5,7 @@ (defn button [label accessibility-label handler] [react/view {:style {:width 50 - :height 40 + :height 30 :justify-content :center :align-items :center}} [react/text @@ -16,7 +16,7 @@ (defn test-menu [] [react/view {:style {:position :absolute - :top 100 + :top 70 :right 0 :width 50 :justify-content :center diff --git a/src/status_im/mailserver/core.cljs b/src/status_im/mailserver/core.cljs index f530ea178f..0057aa83dc 100644 --- a/src/status_im/mailserver/core.cljs +++ b/src/status_im/mailserver/core.cljs @@ -234,12 +234,12 @@ (fx/defn connect-to-mailserver "Add mailserver as a peer using `::add-peer` cofx and generate sym-key when - it doesn't exists - Peer summary will change and we will receive a signal from status go when - this is successful - A connection-check is made after `connection timeout` is reached and - mailserver-state is changed to error if it is not connected by then - No attempt is made if mailserver usage is disabled" + it doesn't exists + Peer summary will change and we will receive a signal from status go when + this is successful + A connection-check is made after `connection timeout` is reached and + mailserver-state is changed to error if it is not connected by then + No attempt is made if mailserver usage is disabled" {:events [:mailserver.ui/reconnect-mailserver-pressed]} [{:keys [db] :as cofx}] (let [{:keys [address]} (fetch-current db) diff --git a/src/status_im/mailserver/topics.cljs b/src/status_im/mailserver/topics.cljs index 3476fda576..3143ad9600 100644 --- a/src/status_im/mailserver/topics.cljs +++ b/src/status_im/mailserver/topics.cljs @@ -96,7 +96,7 @@ (fx/defn update-many [cofx mailserver-topics] (apply fx/merge cofx (map (partial update-topic true) mailserver-topics))) -(fx/defn delete [{:keys [db] :as cofx} {:keys [chat-id filter-id]}] +(fx/defn delete [{:keys [db] :as cofx} {:keys [filter-id]}] (when-let [matching-topics (filter (fn [{:keys [filter-ids] :as topic}] (if (not filter-ids) (do (log/warn "topic not initialized, removing" topic) diff --git a/src/status_im/multiaccounts/login/core.cljs b/src/status_im/multiaccounts/login/core.cljs index ebe8a42581..d0b545a4c2 100644 --- a/src/status_im/multiaccounts/login/core.cljs +++ b/src/status_im/multiaccounts/login/core.cljs @@ -14,6 +14,7 @@ [status-im.native-module.core :as status] [status-im.notifications.core :as notifications] [status-im.popover.core :as popover] + [status-im.communities.core :as communities] [status-im.protocol.core :as protocol] [status-im.stickers.core :as stickers] [status-im.ui.screens.mobile-network-settings.events :as mobile-network] @@ -221,6 +222,7 @@ (protocol/initialize-protocol {:default-mailserver true}) (check-network-version network-id) (chat.loading/initialize-chats) + (communities/fetch) (contact/initialize-contacts) (stickers/init-stickers-packs) (mobile-network/on-network-status-change) @@ -293,6 +295,8 @@ :mailserver-ranges {} :mailserver-topics {} :default-mailserver true}) + + (communities/fetch) (multiaccounts/switch-preview-privacy-mode-flag) (link-preview/request-link-preview-whitelist) (logging/set-log-level (:log-level multiaccount))))) diff --git a/src/status_im/subs.cljs b/src/status_im/subs.cljs index 1d6af977ea..c03b356950 100644 --- a/src/status_im/subs.cljs +++ b/src/status_im/subs.cljs @@ -14,6 +14,7 @@ [status-im.ethereum.transactions.core :as transactions] [status-im.fleet.core :as fleet] [status-im.group-chats.db :as group-chats.db] + [status-im.communities.core :as communities] [status-im.group-chats.core :as group-chat] [status-im.i18n :as i18n] [status-im.multiaccounts.core :as multiaccounts] @@ -217,8 +218,58 @@ (reg-root-key-sub :push-notifications/preferences :push-notifications/preferences) (reg-root-key-sub :acquisition :acquisition) + +;; communities +(re-frame/reg-sub + :communities + (fn [db] + (cond + config/communities-management-enabled? + (:communities db) + config/communities-enabled? + ;; If no management enabled, only return status-community + (select-keys (:communities db) [constants/status-community-id]) + :else + []))) + +(re-frame/reg-sub + :communities/community + :<- [:communities] + (fn [communities [_ id]] + (get communities id))) + +(re-frame/reg-sub + :communities/status-community + :<- [:search/home-filter] + :<- [:communities] + (fn [[search-filter communities]] + (let [status-community (get communities constants/status-community-id)] + (when (and (:joined status-community) + (or (string/blank? search-filter) + (string/includes? (string/lower-case + (get-in status-community [:description :identity :display-name])) search-filter))) + status-community)))) + +(re-frame/reg-sub + :communities/current-community + :<- [:communities] + :<- [:chats/current-raw-chat] + (fn [[communities {:keys [community-id]}]] + (get communities community-id))) + +(re-frame/reg-sub + :communities/unviewed-count + (fn [[_ community-id]] + [(re-frame/subscribe [:chats/by-community-id community-id])]) + (fn [[chats]] + (reduce (fn [acc {:keys [unviewed-messages-count]}] + (+ acc (or unviewed-messages-count 0))) + 0 + chats))) + ;;GENERAL ============================================================================================================== + (re-frame/reg-sub :multiaccount/logged-in? (fn [db] @@ -616,6 +667,16 @@ (fn [chats [_ chat-id]] (get chats chat-id))) +(re-frame/reg-sub + :chats/by-community-id + :<- [:chats/active-chats] + (fn [chats [_ community-id]] + (->> chats + (keep (fn [[_ chat]] + (when (= (:community-id chat) community-id) + chat))) + (sort-by :timestamp >)))) + (re-frame/reg-sub :chats/current-chat-ui-props :<- [::chat-ui-props] @@ -678,8 +739,11 @@ :<- [:chats/current-raw-chat] :<- [:multiaccount/public-key] :<- [:inactive-chat-id] + :<- [:communities/current-community] (fn [[{:keys [group-chat] :as current-chat} - my-public-key inactive-chat-id]] + my-public-key + inactive-chat-id + community]] (when (and current-chat (= (:chat-id current-chat) inactive-chat-id)) (cond-> current-chat (chat.models/public-chat? current-chat) @@ -690,6 +754,10 @@ (assoc :show-input? true :joined? true) + (and (chat.models/community-chat? current-chat) + (communities/can-post? community my-public-key (:chat-id current-chat))) + (assoc :show-input? true) + (not group-chat) (assoc :show-input? true))))) @@ -698,7 +766,14 @@ :<- [:chats/current-raw-chat] (fn [current-chat] (select-keys current-chat - [:public? :group-chat :chat-id :chat-name :color :invitation-admin]))) + [:community-id + :public? + :group-chat + :chat-type + :chat-id + :chat-name + :color + :invitation-admin]))) (re-frame/reg-sub :current-chat/one-to-one-chat? @@ -2011,7 +2086,10 @@ (vals chats)) (vals chats))] - (sort-by :timestamp > filtered-chats)))) + (sort-by :timestamp > (filter (fn [{:keys [community-id]}] + ;; Ignore communities + (not community-id)) + filtered-chats))))) (defn extract-currency-attributes [currency] (let [{:keys [code display-name]} (val currency)] diff --git a/src/status_im/transport/filters/core.cljs b/src/status_im/transport/filters/core.cljs index 0b6e860ef2..30ee49da3c 100644 --- a/src/status_im/transport/filters/core.cljs +++ b/src/status_im/transport/filters/core.cljs @@ -2,6 +2,7 @@ "This namespace is used to handle filters loading and unloading from statusgo" (:require [clojure.string :as string] [re-frame.core :as re-frame] + [status-im.constants :as constants] [status-im.contact.db :as contact.db] [status-im.ethereum.json-rpc :as json-rpc] [status-im.mailserver.core :as mailserver] @@ -65,10 +66,8 @@ (fx/defn upsert-group-chat-topics "Update topics for each member of the group chat" [{:keys [db] :as cofx}] - (let [group-chats (filter (fn [{:keys [group-chat - public?]}] - (and group-chat - (not public?))) + (let [group-chats (filter (fn [{:keys [chat-type]}] + (= chat-type constants/private-group-chat-type)) (vals (:chats db)))] (apply fx/merge cofx @@ -195,12 +194,12 @@ ;; shh filters -(defn- responses->filters [{:keys [negotiated - discovery - filterId - chatId - topic - identity]}] +(defn responses->filters [{:keys [negotiated + discovery + filterId + chatId + topic + identity]}] {:chat-id (if (not= identity "") (str "0x" identity) chatId) :id chatId :filter-id filterId @@ -232,6 +231,9 @@ (mailserver/process-next-messages-request)) (set-filters-initialized))) +(fx/defn handle-filters [cofx filters] + (handle-filters-added cofx (map responses->filters filters))) + (fx/defn handle-filters-removed "Called when we remove a filter from status-go, it will update the mailserver topics" diff --git a/src/status_im/transport/message/core.cljs b/src/status_im/transport/message/core.cljs index 733906cf90..bc8c48bdd5 100644 --- a/src/status_im/transport/message/core.cljs +++ b/src/status_im/transport/message/core.cljs @@ -4,7 +4,9 @@ [status-im.chat.models :as models.chat] [status-im.chat.models.reactions :as models.reactions] [status-im.contact.core :as models.contact] + [status-im.communities.core :as models.communities] [status-im.pairing.core :as models.pairing] + [status-im.transport.filters.core :as models.filters] [status-im.data-store.messages :as data-store.messages] [status-im.data-store.reactions :as data-store.reactions] [status-im.data-store.contacts :as data-store.contacts] @@ -23,20 +25,32 @@ (fx/defn handle-message [cofx message] (models.message/receive-one cofx message)) +(fx/defn handle-community [cofx community] + (models.communities/handle-community cofx community)) + (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 handle-filters [cofx filters] + (models.filters/handle-filters cofx filters)) + +(fx/defn handle-filters-removed [cofx filters] + (models.filters/handle-filters-removed cofx filters)) + (fx/defn process-response {:events [::process]} [cofx ^js response-js] - (let [^js chats (.-chats response-js) + (let [^js communities (.-communities response-js) + ^js chats (.-chats response-js) ^js contacts (.-contacts response-js) ^js installations (.-installations response-js) ^js messages (.-messages response-js) ^js emoji-reactions (.-emojiReactions response-js) + ^js filters (.-filters response-js) + ^js removed-filters (.-removedFilters response-js) ^js invitations (.-invitations response-js)] (cond (seq installations) @@ -53,6 +67,11 @@ {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} (handle-contacts (map data-store.contacts/<-rpc contacts-clj)))) + (seq communities) + (let [community (.pop communities)] + (fx/merge cofx + {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} + (handle-community (types/js->clj community)))) (seq chats) (let [chats-clj (types/js->clj chats)] (js-delete response-js "chats") @@ -81,7 +100,20 @@ (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))))))) + (handle-invitations (map data-store.invitations/<-rpc invitations)))) + (seq filters) + (let [filters (types/js->clj filters)] + (js-delete response-js "filters") + (fx/merge cofx + {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} + (handle-filters filters))) + + (seq removed-filters) + (let [removed-filters (types/js->clj removed-filters)] + (js-delete response-js "removedFilters") + (fx/merge cofx + {:utils/dispatch-later [{:ms 20 :dispatch [::process response-js]}]} + (handle-filters-removed filters)))))) (fx/defn remove-hash [{:keys [db] :as cofx} envelope-hash] diff --git a/src/status_im/transport/message/protocol.cljs b/src/status_im/transport/message/protocol.cljs index 8975667cfe..d7f882ce2b 100644 --- a/src/status_im/transport/message/protocol.cljs +++ b/src/status_im/transport/message/protocol.cljs @@ -9,6 +9,7 @@ text response-to ens-name + community-id image-path audio-path audio-duration-ms @@ -21,6 +22,7 @@ :imagePath image-path :audioPath audio-path :audioDurationMs audio-duration-ms + :communityId community-id :sticker sticker :contentType content-type}) diff --git a/src/status_im/ui/components/chat_icon/screen.cljs b/src/status_im/ui/components/chat_icon/screen.cljs index 0032366e8b..5212f18707 100644 --- a/src/status_im/ui/components/chat_icon/screen.cljs +++ b/src/status_im/ui/components/chat_icon/screen.cljs @@ -24,13 +24,6 @@ (second name) (first name)))]])) -(defn dapp-badge [{:keys [online-view-wrapper online-view online-dot-left online-dot-right]}] - [react/view online-view-wrapper - [react/view online-view - [react/view - [react/view online-dot-left] - [react/view online-dot-right]]]]) - (defn chat-icon-view [chat-id group-chat name styles] [react/view (:container styles) @@ -43,10 +36,6 @@ [chat-id group-chat name color] [chat-icon-view chat-id group-chat name {:container styles/container-chat-toolbar - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right :size 36 :chat-icon styles/chat-icon-chat-toolbar :default-chat-icon (styles/default-chat-icon-chat-toolbar color) @@ -56,10 +45,6 @@ [chat-id group-chat name color] [chat-icon-view chat-id group-chat name {:container styles/container-chat-list - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right :size 40 :chat-icon styles/chat-icon-chat-list :default-chat-icon (styles/default-chat-icon-chat-list color) @@ -69,10 +54,6 @@ [chat-id group-chat name color] [chat-icon-view chat-id group-chat name {:container styles/container-chat-list - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right :size 40 :chat-icon styles/chat-icon-chat-list :default-chat-icon (styles/default-chat-icon-chat-list color) @@ -85,13 +66,9 @@ :default-chat-icon-text (styles/default-chat-icon-text (or size 40))}]]) (defn contact-icon-view - [{:keys [name dapp?] :as contact} {:keys [container] :as styles}] + [contact {:keys [container] :as styles}] [react/view container - (if dapp? - [default-chat-icon name styles] - [photos/photo (multiaccounts/displayed-photo contact) styles]) - (when dapp? - [dapp-badge styles])]) + [photos/photo (multiaccounts/displayed-photo contact) styles]]) (defn contact-icon-contacts-tab [photo-path] [react/view styles/container-chat-list @@ -100,10 +77,6 @@ (defn dapp-icon-permission [contact size] [contact-icon-view contact {:container {:width size :height size} - :online-view-wrapper styles/online-view-wrapper - :online-view styles/online-view - :online-dot-left styles/online-dot-left - :online-dot-right styles/online-dot-right :size size :chat-icon (styles/custom-size-icon size) :default-chat-icon (styles/default-chat-icon-profile colors/default-chat-color size) @@ -118,9 +91,6 @@ (defn profile-icon-view [photo-path name color edit? size override-styles] (let [styles (merge {:container {:width size :height size} - :online-view styles/online-view-profile - :online-dot-left styles/online-dot-left-profile - :online-dot-right styles/online-dot-right-profile :size size :chat-icon styles/chat-icon-profile :default-chat-icon (styles/default-chat-icon-profile color size) diff --git a/src/status_im/ui/components/colors.cljs b/src/status_im/ui/components/colors.cljs index 3856ecc879..ee5a77b1da 100644 --- a/src/status_im/ui/components/colors.cljs +++ b/src/status_im/ui/components/colors.cljs @@ -94,6 +94,7 @@ (def mention-outgoing "#9EE8FA") (def text black) (def text-gray gray) +(def default-community-color "#773377") (def default-chat-color "#a187d5") ;; legacy diff --git a/src/status_im/ui/screens/chat/group.cljs b/src/status_im/ui/screens/chat/group.cljs index e16df3ac2d..3d93fbad3c 100644 --- a/src/status_im/ui/screens/chat/group.cljs +++ b/src/status_im/ui/screens/chat/group.cljs @@ -2,12 +2,12 @@ (:require [re-frame.core :as re-frame] [quo.core :as quo] [status-im.ui.components.react :as react] + [status-im.constants :as constants] [status-im.utils.universal-links.utils :as links] [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.constants :as constants] [status-im.utils.debounce :as debounce]) (:require-macros [status-im.utils.views :refer [defview letsubs]])) @@ -83,18 +83,33 @@ :size :small :color colors/gray}]]) -(defview no-messages-group-chat-description-container [chat-id] +(defn calculate-quiet-time [highest-request-to + lowest-request-from] + (let [quiet-hours (quot (- highest-request-to lowest-request-from) + (* 60 60))] + (if (<= quiet-hours 24) + (i18n/label :t/quiet-hours + {:quiet-hours quiet-hours}) + (i18n/label :t/quiet-days + {:quiet-days (quot quiet-hours 24)})))) + +(defview no-messages-community-chat-description-container [chat-id] + (letsubs [{:keys [highest-request-to lowest-request-from]} + [:mailserver/ranges-by-chat-id chat-id]] + [react/text {:style (merge style/intro-header-description + {:margin-bottom 36})} + (let [quiet-time (calculate-quiet-time highest-request-to + lowest-request-from)] + (i18n/label :t/empty-chat-description-community + {:quiet-hours quiet-time}))])) + +(defview no-messages-private-group-chat-description-container [chat-id] (letsubs [{:keys [highest-request-to lowest-request-from]} [:mailserver/ranges-by-chat-id chat-id]] [react/nested-text {:style (merge style/intro-header-description {:margin-bottom 36})} - (let [quiet-hours (quot (- highest-request-to lowest-request-from) - (* 60 60)) - quiet-time (if (<= quiet-hours 24) - (i18n/label :t/quiet-hours - {:quiet-hours quiet-hours}) - (i18n/label :t/quiet-days - {:quiet-days (quot quiet-hours 24)}))] + (let [quiet-time (calculate-quiet-time highest-request-to + lowest-request-from)] (i18n/label :t/empty-chat-description-public {:quiet-hours quiet-time})) [{:style {:color colors/blue} @@ -143,20 +158,23 @@ (i18n/label :t/membership-description)]) (defn group-chat-description-container - [{:keys [public? - invitation-admin + [{:keys [invitation-admin chat-id chat-name + chat-type loading-messages? no-messages?]}] (cond loading-messages? group-chat-description-loading - (and no-messages? public?) - [no-messages-group-chat-description-container chat-id] + (and no-messages? (= chat-type constants/public-chat-type)) + [no-messages-private-group-chat-description-container chat-id] + + (and no-messages? (= chat-type constants/community-chat-type)) + [no-messages-community-chat-description-container chat-id] invitation-admin [group-chat-membership-description] - (not public?) + (= chat-type constants/private-group-chat-type) [group-chat-inviter-description-container chat-id chat-name])) diff --git a/src/status_im/ui/screens/chat/message/message.cljs b/src/status_im/ui/screens/chat/message/message.cljs index ca188ddd47..34216f9683 100644 --- a/src/status_im/ui/screens/chat/message/message.cljs +++ b/src/status_im/ui/screens/chat/message/message.cljs @@ -2,6 +2,9 @@ (:require [re-frame.core :as re-frame] [status-im.constants :as constants] [status-im.i18n :as i18n] + [status-im.communities.core :as communities] + [status-im.utils.config :as config] + [status-im.react-native.resources :as resources] [status-im.ui.components.colors :as colors] [status-im.ui.components.icons.vector-icons :as vector-icons] [status-im.ui.components.react :as react] @@ -10,6 +13,7 @@ [status-im.ui.screens.chat.message.command :as message.command] [status-im.ui.screens.chat.photos :as photos] [status-im.ui.screens.chat.sheets :as sheets] + [status-im.ui.components.chat-icon.screen :as chat-icon] [status-im.ui.screens.chat.styles.message.message :as style] [status-im.ui.screens.chat.utils :as chat.utils] [status-im.utils.contenthash :as contenthash] @@ -209,6 +213,64 @@ (letsubs [contact-with-names [:multiaccount/contact]] (chat.utils/format-author contact-with-names opts))) +(defview community-content [{:keys [community-id] :as message}] + (letsubs [{:keys [joined verified] :as community} [:communities/community community-id]] + (when (and + config/communities-enabled? + community) + [react/view {:style (assoc (style/message-wrapper message) + :margin-vertical 10 + :width 271)} + (when verified + [react/view {:border-right-width 1 + :border-left-width 1 + :border-top-width 1 + :border-left-color colors/gray-lighter + :border-right-color colors/gray-lighter + :border-top-left-radius 10 + :border-top-right-radius 10 + :padding-vertical 8 + :padding-horizontal 15 + :border-top-color colors/gray-lighter} + [react/text {:style {:font-size 13 + :color colors/blue}} (i18n/label :t/communities-verified)]]) + + [react/view {:flex-direction :row + :padding-vertical 12 + :border-top-left-radius (when-not verified 10) + :border-top-right-radius (when-not verified 10) + :border-right-width 1 + :border-left-width 1 + :border-top-width 1 + :border-color colors/gray-lighter} + + [react/view {:width 62 + :padding-left 14} + (if (= community-id constants/status-community-id) + [react/image {:source (resources/get-image :status-logo) + :style {:width 40 + :height 40}}] + + (let [display-name (get-in community [:description :identity :display-name])] + [chat-icon/chat-icon-view-chat-list + display-name + true + display-name + colors/default-community-color]))] + [react/view {:padding-right 14} + [react/text {:style {:font-weight "700" + :font-size 17}} + (get-in community [:description :identity :display-name])] + [react/text (get-in community [:description :identity :description])]]] + [react/view {:border-width 1 + :padding-vertical 8 + :border-bottom-left-radius 10 + :border-bottom-right-radius 10 + :border-color colors/gray-lighter} + [react/touchable-highlight {:on-press #(re-frame/dispatch [(if joined ::communities/leave ::communities/join) (:id community)])} + [react/text {:style {:text-align :center + :color colors/blue}} (if joined (i18n/label :t/leave) (i18n/label :t/join))]]]]))) + (defn message-content-wrapper "Author, userpic and delivery wrapper" [{:keys [first-in-group? display-photo? display-username? @@ -356,6 +418,10 @@ [collapsible-text-message message on-long-press modal] reaction-picker]) +(defmethod ->message constants/content-type-community + [message] + [community-content message]) + (defmethod ->message constants/content-type-status [{:keys [content content-type] :as message}] [message-content-wrapper message diff --git a/src/status_im/ui/screens/chat/sheets.cljs b/src/status_im/ui/screens/chat/sheets.cljs index e3121c830f..83ca13092d 100644 --- a/src/status_im/ui/screens/chat/sheets.cljs +++ b/src/status_im/ui/screens/chat/sheets.cljs @@ -2,6 +2,7 @@ (:require [re-frame.core :as re-frame] [status-im.i18n :as i18n] [status-im.ui.components.react :as react] + [status-im.constants :as constants] [status-im.ui.components.list-selection :as list-selection] [status-im.utils.universal-links.utils :as universal-links] [status-im.ui.components.chat-icon.screen :as chat-icon] @@ -78,6 +79,27 @@ :icon :main-icons/delete :on-press #(re-frame/dispatch [:chat.ui/remove-chat-pressed chat-id])}]])) +(defn community-chat-accents [] + (fn [{:keys [chat-id group-chat chat-name color]}] + [react/view + [quo/list-item + {:theme :accent + :title chat-name + :icon [chat-icon/chat-icon-view-chat-sheet + chat-id group-chat chat-name color]}] + [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])}]])) + (defn group-chat-accents [] (fn [{:keys [chat-id group-chat chat-name color invitation-admin]}] (let [{:keys [joined?]} @(re-frame/subscribe [:group-chat/inviter-info chat-id])] @@ -117,11 +139,20 @@ :icon :main-icons/arrow-left :on-press #(re-frame/dispatch [:group-chats.ui/leave-chat-pressed chat-id])}])])))) -(defn actions [{:keys [public? group-chat] +(defn actions [{:keys [chat-type] :as current-chat}] (cond - public? [public-chat-accents current-chat] - group-chat [group-chat-accents current-chat] + (#{constants/public-chat-type + constants/profile-chat-type + constants/timeline-chat-type} chat-type) + [public-chat-accents current-chat] + + (= chat-type constants/community-chat-type) + [community-chat-accents current-chat] + + (= chat-type constants/private-group-chat-type) + [group-chat-accents current-chat] + :else [one-to-one-chat-accents current-chat])) (defn options [chat-id message-id] diff --git a/src/status_im/ui/screens/chat/toolbar_content.cljs b/src/status_im/ui/screens/chat/toolbar_content.cljs index fe6a6d2cfb..b0ef6453ee 100644 --- a/src/status_im/ui/screens/chat/toolbar_content.cljs +++ b/src/status_im/ui/screens/chat/toolbar_content.cljs @@ -1,5 +1,6 @@ (ns status-im.ui.screens.chat.toolbar-content (:require [status-im.i18n :as i18n] + [status-im.constants :as constants] [status-im.ui.components.chat-icon.screen :as chat-icon.screen] [status-im.ui.components.react :as react] [status-im.ui.screens.chat.styles.main :as st] @@ -35,6 +36,7 @@ color chat-id contacts + chat-type chat-name public?]}] [react/view {:style st/toolbar-container} @@ -49,6 +51,6 @@ [one-to-one-name chat-id]) (when-not group-chat [contact-indicator chat-id]) - (when (and group-chat (not invitation-admin)) + (when (and group-chat (not invitation-admin) (not= chat-type constants/community-chat-type)) [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 10b8ad6c3d..db8c60ff1c 100644 --- a/src/status_im/ui/screens/chat/views.cljs +++ b/src/status_im/ui/screens/chat/views.cljs @@ -74,10 +74,10 @@ (defn chat-intro [{:keys [chat-id chat-name + chat-type group-chat invitation-admin contact-name - public? color loading-messages? no-messages?]}] @@ -99,7 +99,7 @@ :invitation-admin invitation-admin :loading-messages? loading-messages? :chat-name chat-name - :public? public? + :chat-type chat-type :no-messages? no-messages?}] [react/text {:style (assoc style/intro-header-description :margin-bottom 32)} @@ -115,6 +115,7 @@ (defn chat-intro-header-container [{:keys [group-chat invitation-admin + chat-type might-have-join-time-messages? color chat-id chat-name public?]} @@ -128,6 +129,7 @@ {:chat-id chat-id :group-chat group-chat :invitation-admin invitation-admin + :chat-type chat-type :chat-name chat-name :public? public? :color color @@ -170,7 +172,7 @@ (defn messages-view [{:keys [chat bottom-space pan-responder space-keeper]}] - (let [{:keys [group-chat chat-id public? invitation-admin]} chat + (let [{:keys [group-chat chat-id chat-type public? invitation-admin]} chat messages @(re-frame/subscribe [:chats/current-chat-messages-stream]) no-messages? @(re-frame/subscribe [:chats/current-chat-no-messages?]) @@ -180,11 +182,11 @@ pan-responder {:key-fn #(or (:message-id %) (:value %)) :ref #(reset! messages-list-ref %) - :header (when (and group-chat (not public?)) + :header (when (= chat-type constants/private-group-chat-type) [chat.group/group-chat-footer chat-id invitation-admin]) :footer [:<> [chat-intro-header-container chat no-messages?] - (when (and (not group-chat) (not public?)) + (when (= chat-type constants/one-to-one-chat-type) [invite.chat/reward-messages])] :data messages :inverted true diff --git a/src/status_im/ui/screens/communities/views.cljs b/src/status_im/ui/screens/communities/views.cljs new file mode 100644 index 0000000000..cc2a97b299 --- /dev/null +++ b/src/status_im/ui/screens/communities/views.cljs @@ -0,0 +1,423 @@ +(ns status-im.ui.screens.communities.views (:require-macros [status-im.utils.views :as views]) + (:require + [reagent.core :as reagent] + [re-frame.core :as re-frame] + [quo.core :as quo] + [status-im.i18n :as i18n] + [status-im.utils.core :as utils] + [status-im.utils.config :as config] + [status-im.constants :as constants] + [status-im.communities.core :as communities] + [status-im.ui.screens.home.views.inner-item :as inner-item] + [status-im.ui.screens.home.styles :as home.styles] + [status-im.ui.components.list.views :as list] + [status-im.ui.components.copyable-text :as copyable-text] + [status-im.react-native.resources :as resources] + [status-im.ui.components.topbar :as topbar] + [status-im.ui.components.icons.vector-icons :as icons] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.chat-icon.screen :as chat-icon.screen] + [status-im.ui.components.toolbar :as toolbar] + [status-im.ui.components.react :as react])) + +(defn hide-sheet-and-dispatch [event] + (re-frame/dispatch [:bottom-sheet/hide]) + (re-frame/dispatch event)) + +(defn community-list-item [{:keys [id description]}] + (let [identity (:identity description)] + [quo/list-item + {:icon (if (= id constants/status-community-id) + [react/image {:source (resources/get-image :status-logo) + :style {:width 40 + :height 40}}] + + [chat-icon.screen/chat-icon-view-chat-list + id + true + (:display-name identity) + ;; TODO: should be derived by id + (or (:color identity) + (rand-nth colors/chat-colors)) + false + false]) + :title [react/view {:flex-direction :row + :flex 1} + [react/view {:flex-direction :row + :flex 1 + :padding-right 16 + :align-items :center} + [quo/text {:weight :medium + :accessibility-label :community-name-text + :ellipsize-mode :tail + :number-of-lines 1} + (utils/truncate-str (:display-name identity) 30)]]] + :title-accessibility-label :community-name-text + :subtitle [react/view {:flex-direction :row} + [react/view {:flex 1} + [quo/text + (utils/truncate-str (:description identity) 30)]]] + :on-press #(do + (re-frame/dispatch [:dismiss-keyboard]) + (re-frame/dispatch [:navigate-to :community id]))}])) + +(defn communities-actions [] + [react/view + [quo/list-item + {:theme :accent + :title (i18n/label :t/import-community) + :accessibility-label :community-import-community + :icon :main-icons/check + :on-press #(hide-sheet-and-dispatch [::communities/import-pressed])}] + [quo/list-item + {:theme :accent + :title (i18n/label :t/create-community) + :accessibility-label :community-create-community + :icon :main-icons/check + :on-press #(hide-sheet-and-dispatch [::communities/create-pressed])}]]) + +(views/defview communities [] + (views/letsubs [communities [:communities]] + [react/view {:flex 1} + [topbar/topbar (cond-> {:title (i18n/label :t/communities)} + config/communities-management-enabled? + (assoc :right-accessories [{:icon :main-icons/more + :accessibility-label :chat-menu-button + :on-press + #(re-frame/dispatch [:bottom-sheet/show-sheet + {:content (fn [] + [communities-actions]) + :height 256}])}]))] + [react/scroll-view {:style {:flex 1} + :content-container-style {:padding-vertical 8}} + [list/flat-list + {:key-fn :id + :keyboard-should-persist-taps :always + :data (vals communities) + :render-fn (fn [community] [community-list-item community])}]] + (when config/communities-management-enabled? + [toolbar/toolbar + {:show-border? true + :center [quo/button {:on-press #(re-frame/dispatch [::communities/create-pressed])} + (i18n/label :t/create)]}])])) + +(defn valid? [community-name community-description] + (and (not= "" community-name) + (not= "" community-description))) + +(defn import-community [] + (let [community-key (reagent/atom "")] + (fn [] + [react/view {:style {:padding-left 16 + :padding-right 8}} + [react/view {:style {:padding-horizontal 20}} + [quo/text-input + {:label (i18n/label :t/community-key) + :placeholder (i18n/label :t/community-key-placeholder) + :on-change-text #(reset! community-key %) + :auto-focus true}]] + [react/view {:style {:padding-top 20 + :padding-horizontal 20}} + [quo/button {:disabled (= @community-key "") + :on-press #(re-frame/dispatch [::communities/import-confirmation-pressed @community-key])} + (i18n/label :t/import)]]]))) + +(defn create [] + (let [community-name (reagent/atom "") + membership (reagent/atom 1) + community-description (reagent/atom "")] + (fn [] + [react/view {:style {:padding-left 16 + :padding-right 8}} + [react/view {:style {:padding-horizontal 20}} + [quo/text-input + {:label (i18n/label :t/name-your-community) + :placeholder (i18n/label :t/name-your-community-placeholder) + :on-change-text #(reset! community-name %) + :auto-focus true}]] + [react/view {:style {:padding-horizontal 20}} + [quo/text-input + {:label (i18n/label :t/give-a-short-description-community) + :placeholder (i18n/label :t/give-a-short-description-community) + :multiline true + :number-of-lines 4 + :on-change-text #(reset! community-description %)}]] + [react/view {:style {:padding-horizontal 20}} + [quo/text-input + {:label (i18n/label :t/membership-type) + :placeholder (i18n/label :t/membership-type-placeholder) + :on-change-text #(reset! membership %)}]] + + [react/view {:style {:padding-top 20 + :padding-horizontal 20}} + [quo/button {:disabled (not (valid? @community-name @community-description)) + :on-press #(re-frame/dispatch [::communities/create-confirmation-pressed @community-name @community-description @membership])} + (i18n/label :t/create)]]]))) + +(def create-sheet + {:content create}) + +(def import-sheet + {:content import-community}) + +(defn create-channel [] + (let [channel-name (reagent/atom "") + channel-description (reagent/atom "")] + (fn [] + [react/view {:style {:padding-left 16 + :padding-right 8}} + [react/view {:style {:padding-horizontal 20}} + [quo/text-input + {:label (i18n/label :t/name-your-channel) + :placeholder (i18n/label :t/name-your-channel-placeholder) + :on-change-text #(reset! channel-name %) + :auto-focus true}]] + [react/view {:style {:padding-horizontal 20}} + [quo/text-input + {:label (i18n/label :t/give-a-short-description-channel) + :placeholder (i18n/label :t/give-a-short-description-channel) + :multiline true + :number-of-lines 4 + :on-change-text #(reset! channel-description %)}]] + + (when config/communities-management-enabled? + [react/view {:style {:padding-top 20 + :padding-horizontal 20}} + [quo/button {:disabled (not (valid? @channel-name @channel-description)) + :on-press #(re-frame/dispatch [::communities/create-channel-confirmation-pressed @channel-name @channel-description])} + (i18n/label :t/create)]])]))) + +(def create-channel-sheet + {:content create-channel}) + +(defn invite-people [] + (let [user-pk (reagent/atom "")] + (fn [] + [react/view {:style {:padding-left 16 + :padding-right 8}} + [react/view {:style {:padding-horizontal 20}} + [quo/text-input + {:label (i18n/label :t/enter-user-pk) + :placeholder (i18n/label :t/enter-user-pk) + :on-change-text #(reset! user-pk %) + :auto-focus true}]] + [react/view {:style {:padding-top 20 + :padding-horizontal 20}} + [quo/button {:disabled (= "" user-pk) + :on-press #(re-frame/dispatch [::communities/invite-people-confirmation-pressed @user-pk])} + (i18n/label :t/invite)]]]))) + +(def invite-people-sheet + {:content invite-people}) + +(defn community-actions [id admin] + [react/view + (when (and config/communities-management-enabled? admin) + [quo/list-item + {:theme :accent + :title (i18n/label :t/export-key) + :accessibility-label :community-export-key + :icon :main-icons/check + :on-press #(hide-sheet-and-dispatch [::communities/export-pressed id])}]) + (when (and config/communities-management-enabled? admin) + [quo/list-item + {:theme :accent + :title (i18n/label :t/create-channel) + :accessibility-label :community-create-channel + :icon :main-icons/check + :on-press #(hide-sheet-and-dispatch [::communities/create-channel-pressed id])}]) + (when (and config/communities-management-enabled? admin) + [quo/list-item + {:theme :accent + :title (i18n/label :t/invite-people) + :accessibility-label :community-invite-people + :icon :main-icons/close + :on-press #(re-frame/dispatch [::communities/invite-people-pressed id])}]) + [quo/list-item + {:theme :accent + :title (i18n/label :t/leave) + :accessibility-label :leave + :icon :main-icons/close + :on-press #(do + (re-frame/dispatch [:navigate-to :home]) + (re-frame/dispatch [:bottom-sheet/hide]) + (re-frame/dispatch [::communities/leave id]))}]]) + +(defn toolbar-content [id display-name color] + [react/view {:style {:flex 1 + :align-items :center + :flex-direction :row}} + [react/view {:margin-right 10} + (if (= id constants/status-community-id) + [react/image {:source (resources/get-image :status-logo) + :style {:width 40 + :height 40}}] + [chat-icon.screen/chat-icon-view-toolbar + id + true + display-name + (or color + (rand-nth colors/chat-colors))])] + [react/view {:style {:flex 1 :justify-content :center}} + [react/text {:style {:typography :main-medium + :font-size 15 + :line-height 22} + :number-of-lines 1 + :accessibility-label :community-name-text} + display-name]]]) + +(defn topbar [id display-name color admin joined] + [topbar/topbar + {:content [toolbar-content id display-name color] + :navigation {:on-press #(re-frame/dispatch [:navigate-back])} + :right-accessories (when (or admin joined) + [{:icon :main-icons/more + :accessibility-label :community-menu-button + :on-press + #(re-frame/dispatch [:bottom-sheet/show-sheet + {:content (fn [] + [community-actions id admin]) + :height 256}])}])}]) + +(defn welcome-blank-page [] + [react/view {:style {:flex 1 :flex-direction :row :align-items :center :justify-content :center}} + [react/i18n-text {:style home.styles/welcome-blank-text :key :welcome-blank-message}]]) + +(views/defview community-unviewed-count [id] + (views/letsubs [unviewed-count [:communities/unviewed-count id]] + (when-not (zero? unviewed-count) + [react/view {:style {:background-color colors/blue + :border-radius 6 + :margin-right 5 + :margin-top 2 + :width 12 + :height 12} + :accessibility-label :unviewed-messages-public}]))) + +(defn status-community [{:keys [id description]}] + [quo/list-item + {:icon [react/image {:source (resources/get-image :status-logo) + :style {:width 40 + :height 40}}] + :title [react/view {:flex-direction :row + :flex 1} + [react/view {:flex-direction :row + :flex 1 + :padding-right 16 + :align-items :center} + [quo/text {:weight :medium + :accessibility-label :chat-name-text + :font-size 17 + :ellipsize-mode :tail + :number-of-lines 1} + (get-in description [:identity :display-name])]] + [react/view {:flex-direction :row + :flex 1 + :justify-content :flex-end + :align-items :center} + [community-unviewed-count id]]] + + :title-accessibility-label :chat-name-text + :on-press #(do + (re-frame/dispatch [:dismiss-keyboard]) + (re-frame/dispatch [:navigate-to :community id])) + ;; TODO: actions + :on-long-press #(re-frame/dispatch [:bottom-sheet/show-sheet + nil])}]) + +(defn channel-preview-item [{:keys [id identity]}] + [quo/list-item + {:icon [chat-icon.screen/chat-icon-view-chat-list + id true (:display-name identity) colors/blue false false] + :title [react/view {:flex-direction :row + :flex 1} + [react/view {:flex-direction :row + :flex 1 + :padding-right 16 + :align-items :center} + [icons/icon :main-icons/tiny-group + {:color colors/black + :width 15 + :height 15 + :container-style {:width 15 + :height 15 + :margin-right 2}}] + [quo/text {:weight :medium + :accessibility-label :chat-name-text + :ellipsize-mode :tail + :number-of-lines 1} + (utils/truncate-str (:display-name identity) 30)]]] + :title-accessibility-label :chat-name-text + :subtitle [react/view {:flex-direction :row} + [react/text-class {:style home.styles/last-message-text + :number-of-lines 1 + :ellipsize-mode :tail + :accessibility-label :chat-message-text} (:description identity)]]}]) + +(defn community-channel-preview-list [_ description] + (let [chats (reduce-kv + (fn [acc k v] + (conj acc (assoc v :id (name k)))) + [] + (get-in description [:chats]))] + [list/flat-list + {:key-fn :id + :keyboard-should-persist-taps :always + :data chats + :render-fn channel-preview-item}])) + +(defn community-chat-list [chats] + (if (empty? chats) + [welcome-blank-page] + [list/flat-list + {:key-fn :chat-id + :keyboard-should-persist-taps :always + :data chats + :render-fn (fn [home-item] [inner-item/home-list-item (assoc home-item :color colors/blue)]) + :footer [react/view {:height 68}]}])) + +(views/defview community-channel-list [id] + (views/letsubs [chats [:chats/by-community-id id]] + [community-chat-list chats])) + +(views/defview community [route] + (views/letsubs [{:keys [id description joined admin]} [:communities/community (get-in route [:route :params])]] + [react/view {:style {:flex 1}} + [topbar + id + (get-in description [:identity :display-name]) + (get-in description [:identity :color]) + admin + joined] + (if joined + [community-channel-list id] + [community-channel-preview-list id description]) + (when-not joined + [react/view {:style {:padding-top 20 + :margin-bottom 10 + :padding-horizontal 20}} + [quo/button {:on-press #(re-frame/dispatch [::communities/join id])} + (i18n/label :t/join)]])])) + +(views/defview export-community [] + (views/letsubs [{:keys [community-key]} [:popover/popover]] + [react/view {} + [react/view {:style {:padding-top 16 :padding-horizontal 16}} + [copyable-text/copyable-text-view + {:label :t/community-key + :container-style {:margin-top 12 :margin-bottom 4} + :copied-text community-key} + [quo/text {:number-of-lines 1 + :ellipsize-mode :middle + :accessibility-label :chat-key + :monospace true} + community-key]]]])) + +(defn render-featured-community [{:keys [name id]}] + ^{:key id} + [react/touchable-highlight {:on-press #(re-frame/dispatch [:navigate-to :community id]) + :accessibility-label :chat-item} + [react/view {:padding-right 8 :padding-vertical 8} + [react/view {:border-color colors/gray-lighter :border-radius 36 :border-width 1 :padding-horizontal 8 :padding-vertical 5} + [react/text {:style {:color colors/blue :typography :main-medium}} name]]]]) + diff --git a/src/status_im/ui/screens/home/sheet/views.cljs b/src/status_im/ui/screens/home/sheet/views.cljs index 2cc6b60f1f..2f20325492 100644 --- a/src/status_im/ui/screens/home/sheet/views.cljs +++ b/src/status_im/ui/screens/home/sheet/views.cljs @@ -47,6 +47,13 @@ :accessibility-label :join-public-chat-button :icon :main-icons/public-chat :on-press #(hide-sheet-and-dispatch [:navigate-to :new-public-chat])}] + (when config/communities-enabled? + [quo/list-item + {:theme :accent + :title (i18n/label :t/communities-alpha) + :accessibility-label :communities-button + :icon :main-icons/communities + :on-press #(hide-sheet-and-dispatch [:navigate-to :communities])}]) [invite/list-item {:accessibility-label :chats-menu-invite-friends-button}]]) diff --git a/src/status_im/ui/screens/home/views.cljs b/src/status_im/ui/screens/home/views.cljs index 271bd7245a..a19ea8ddc5 100644 --- a/src/status_im/ui/screens/home/views.cljs +++ b/src/status_im/ui/screens/home/views.cljs @@ -3,11 +3,13 @@ [reagent.core :as reagent] [status-im.i18n :as i18n] [status-im.react-native.resources :as resources] + [status-im.communities.core :as communities] [status-im.ui.components.connectivity.view :as connectivity] [status-im.ui.components.icons.vector-icons :as icons] [status-im.ui.components.list.views :as list] [status-im.ui.components.react :as react] [status-im.ui.screens.home.styles :as styles] + [status-im.ui.screens.communities.views :as communities.views] [status-im.ui.screens.home.views.inner-item :as inner-item] [status-im.ui.screens.referrals.home-item :as referral-item] [status-im.ui.components.colors :as colors] @@ -79,7 +81,14 @@ [react/view {:style styles/tags-wrapper} [react/view {:flex-direction :row :flex-wrap :wrap :justify-content :center} (for [chat (new-public-chat/featured-public-chats)] - (new-public-chat/render-topic chat))]]]]) + (new-public-chat/render-topic chat))]] + [react/i18n-text {:style {:margin-horizontal 16 + :text-align :center} + :key :join-a-community}] + [react/view {:style styles/tags-wrapper} + [react/view {:flex-direction :row :flex-wrap :wrap :justify-content :center} + (for [community communities/featured] + (communities.views/render-featured-community community))]]]]) (defn welcome-blank-page [] [react/view {:style {:flex 1 :flex-direction :row :align-items :center :justify-content :center}} @@ -136,31 +145,48 @@ (defn render-fn [home-item] [inner-item/home-list-item home-item]) +(defn communities-and-chats [chats status-community loading? search-filter hide-home-tooltip?] + (if loading? + [react/view {:flex 1 :align-items :center :justify-content :center} + [react/activity-indicator {:animating true}]] + (if (and (empty? chats) + (not status-community) + (empty? search-filter) + hide-home-tooltip? + (not @search-active?)) + [welcome-blank-page] + [react/view + [:<> + (when (or (seq chats) @search-active? (seq search-filter)) + [search-input-wrapper search-filter chats]) + [referral-item/list-item]] + (when + (and (empty? chats) + (not status-community)) + (or @search-active? (seq search-filter)) + [start-suggestion search-filter]) + (when status-community + ;; We only support one community now, Status + [communities.views/status-community status-community]) + (when (and status-community + (seq chats)) + [quo/separator]) + [list/flat-list + {:key-fn :chat-id + :keyboard-should-persist-taps :always + :data chats + :render-fn render-fn + :footer (if (and (not hide-home-tooltip?) (not @search-active?)) + [home-tooltip-view] + [react/view {:height 68}])}]]))) + (views/defview chats-list [] - (views/letsubs [loading? [:chats/loading?] + (views/letsubs [status-community [:communities/status-community] + loading? [:chats/loading?] {:keys [chats search-filter]} [:home-items] {:keys [hide-home-tooltip?]} [:multiaccount]] - (if loading? - [react/view {:flex 1 :align-items :center :justify-content :center} - [react/activity-indicator {:animating true}]] - (if (and (empty? chats) - (empty? search-filter) - hide-home-tooltip? - (not @search-active?)) - [welcome-blank-page] - [list/flat-list - {:key-fn :chat-id - :keyboard-should-persist-taps :always - :data chats - :render-fn render-fn - :header [:<> (when (or (seq chats) @search-active? (seq search-filter)) - [search-input-wrapper search-filter chats]) - [referral-item/list-item]] - :empty-component (when (or @search-active? (seq search-filter)) - [start-suggestion search-filter]) - :footer (if (and (not hide-home-tooltip?) (not @search-active?)) - [home-tooltip-view] - [react/view {:height 68}])}])))) + [react/scroll-view + [communities-and-chats chats status-community loading? search-filter hide-home-tooltip?]])) (views/defview plus-button [] (views/letsubs [logging-in? [:multiaccounts/login]] diff --git a/src/status_im/ui/screens/popover/views.cljs b/src/status_im/ui/screens/popover/views.cljs index 98a5574c27..7f69af20a4 100644 --- a/src/status_im/ui/screens/popover/views.cljs +++ b/src/status_im/ui/screens/popover/views.cljs @@ -6,6 +6,7 @@ [re-frame.core :as re-frame] [status-im.utils.platform :as platform] [status-im.ui.screens.wallet.signing-phrase.views :as signing-phrase] + [status-im.ui.screens.communities.views :as communities] [status-im.ui.screens.wallet.request.views :as request] [status-im.ui.screens.profile.user.views :as profile.user] ["react-native" :refer (BackHandler)] @@ -147,6 +148,9 @@ (= :advertiser-invite view) [advertiser.invite/accept-popover] + (= :export-community view) + [communities/export-community] + (= :dapp-invite view) [dapp.invite/accept-popover] diff --git a/src/status_im/ui/screens/routing/chat_stack.cljs b/src/status_im/ui/screens/routing/chat_stack.cljs index 9717126c4c..79283ae34b 100644 --- a/src/status_im/ui/screens/routing/chat_stack.cljs +++ b/src/status_im/ui/screens/routing/chat_stack.cljs @@ -4,6 +4,7 @@ [status-im.ui.screens.chat.views :as chat] [status-im.ui.screens.group.views :as group] [status-im.ui.screens.referrals.public-chat :as referrals.public-chat] + [status-im.ui.screens.communities.views :as communities] [status-im.ui.screens.profile.group-chat.views :as profile.group-chat] [status-im.ui.components.tabbar.styles :as tabbar.styles] [status-im.ui.screens.stickers.views :as stickers])) @@ -19,6 +20,14 @@ :component home/home} {:name :referral-enclav :component referrals.public-chat/view} + {:name :communities + :transition :presentation-ios + :insets {:bottom true} + :component communities/communities} + {:name :community + :transition :presentation-ios + :insets {:bottom true} + :component communities/community} {:name :chat :component chat/chat} {:name :group-chat-profile diff --git a/src/status_im/ui/screens/routing/main.cljs b/src/status_im/ui/screens/routing/main.cljs index 3de1e13c9c..0c7f54375c 100644 --- a/src/status_im/ui/screens/routing/main.cljs +++ b/src/status_im/ui/screens/routing/main.cljs @@ -154,7 +154,6 @@ :transition :presentation-ios :insets {:bottom true} :component contact/profile}] - (when config/quo-preview-enabled? [{:name :quo-preview :insets {:top false :bottom false} diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs index f30140666c..13f73a5ca3 100644 --- a/src/status_im/ui/screens/views.cljs +++ b/src/status_im/ui/screens/views.cljs @@ -12,6 +12,7 @@ [status-im.ui.screens.routing.main :as routing] [status-im.ui.screens.signing.views :as signing] [status-im.ui.screens.popover.views :as popover] + [status-im.ui.screens.communities.views :as communities] [status-im.ui.screens.multiaccounts.recover.views :as recover.views] [status-im.ui.screens.wallet.send.views :as wallet] [status-im.ui.components.status-bar.view :as statusbar] @@ -49,6 +50,18 @@ (= view :learn-more) (merge about-app/learn-more) + (= view :create-community) + (merge communities/create-sheet) + + (= view :import-community) + (merge communities/import-sheet) + + (= view :create-community-channel) + (merge communities/create-channel-sheet) + + (= view :invite-people-community) + (merge communities/invite-people-sheet) + (= view :recover-sheet) (merge recover.views/bottom-sheet))] [quo/bottom-sheet opts diff --git a/src/status_im/utils/config.cljs b/src/status_im/utils/config.cljs index df9ad7de84..d1934048ce 100644 --- a/src/status_im/utils/config.cljs +++ b/src/status_im/utils/config.cljs @@ -46,6 +46,10 @@ (def referrals-invite-enabled? (enabled? (get-config :ENABLE_REFERRAL_INVITE "0"))) (def quo-preview-enabled? (enabled? (get-config :ENABLE_QUO_PREVIEW "0"))) (def google-free (enabled? (get-config :GOOGLE_FREE "0"))) +(def communities-enabled? (enabled? (get-config :COMMUNITIES_ENABLED "0"))) +(def communities-management-enabled? (and (enabled? (get-config :COMMUNITIES_MANAGEMENT_ENABLED "0")) + communities-enabled?)) + ;; CONFIG VALUES (def log-level (string/upper-case (get-config :LOG_LEVEL ""))) diff --git a/src/status_im/utils/types.cljs b/src/status_im/utils/types.cljs index 8d96e2e974..d446ac2f52 100644 --- a/src/status_im/utils/types.cljs +++ b/src/status_im/utils/types.cljs @@ -20,7 +20,8 @@ (when-not (= json "undefined") (try (js->clj (.parse js/JSON json)) - (catch js/Error _ (when (string? json) json))))) + (catch js/Error _ + (when (string? json) json))))) (def serialize clj->json) (defn deserialize [o] (try (json->clj o) diff --git a/status-go-version.json b/status-go-version.json index 87f945e817..82adf7217b 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.66.2", - "commit-sha1": "fee08aafbe0cdad352a4bdccff2ec883a3443b9c", - "src-sha256": "0q5x70mdya3561dyigmkickhb8hkz92d14cc9hhwmbkpz41bcrd8" + "version": "v0.67.0", + "commit-sha1": "f5482ec187b981dd77d184cc7003dcc0c6ae5d22", + "src-sha256": "18bzsf1nskvsvkxxw9v8v4zpgxfaag7q6a07fbdmz7f0y3a3mxjz" } diff --git a/translations/en.json b/translations/en.json index 542fb33907..da5988a92a 100644 --- a/translations/en.json +++ b/translations/en.json @@ -145,6 +145,17 @@ "close-app-content": "The app will stop and close. When you reopen it, the selected network will be used", "close-app-title": "Warning!", "command-button-send": "Send", + "communities": "Communities", + "name-your-channel": "Name your channel", + "give-a-short-description": "Give a short description", + "communities-alpha": "Communities (alpha)", + "communities-verified": "✓ Verified Status Community", + "create-community": "Create a community", + "create-channel": "Create a channel", + "import-community": "Import a community", + "name-your-community": "Name your community", + "name-your-community-placeholder": "A catchy name", + "give-a-short-description-community": "Give it a short description", "complete-hardwallet-setup": "This card is now linked. You need it to sign transactions and unlock your keys", "completed": "Completed", "confirm": "Confirm", @@ -355,6 +366,7 @@ "empty-chat-description": "There are no messages \nin this chat yet", "empty-chat-description-one-to-one": "Any messages you send here are encrypted and can only be read by you and ", "empty-chat-description-public": "It's been quiet here for the last {{quiet-hours}}. Start the conversation or ", + "empty-chat-description-community": "It's been quiet here for the last {{quiet-hours}}.", "empty-chat-description-public-share-this": "share this chat.", "enable": "Enable", "encrypt-with-password": "Encrypt with password", @@ -466,6 +478,7 @@ "ethereum-node-started-incorrectly-title": "Ethereum node started incorrectly", "etherscan-lookup": "Look up on Etherscan", "export-account": "Export account", + "export-key": "Export key", "failed": "Failed", "faq": "Frequently asked questions", "fetch-messages": "↓ Fetch messages", @@ -574,9 +587,11 @@ "invalid-pairing-password": "Invalid pairing password", "invalid-range": "Invalid format, must be between {{min}} and {{max}}", "join-me": "Hey join me on Status: {{url}}", + "join-a-community": "or join a community", "http-gateway-error": "Oops, request failed!", "sign-request-failed": "Could not sign message", "invite-friends": "Invite friends", + "invite-people": "Invite people", "invite-reward": "Earn crypto for every friend you invite!", "invite-select-account": "Select an account to receive your referral bonus", "invited": "invited", @@ -681,6 +696,7 @@ "learn-more": "Learn more", "learn-more-about-keycard": "Learn more about Keycard", "leave": "Leave", + "joined": "Joined", "leave-group": "Leave group", "left": "left", "lets-go": "Let's go",