diff --git a/src/legacy/status_im/data_store/communities.cljs b/src/legacy/status_im/data_store/communities.cljs index deef5efc1a..dc473aa4e1 100644 --- a/src/legacy/status_im/data_store/communities.cljs +++ b/src/legacy/status_im/data_store/communities.cljs @@ -1,8 +1,8 @@ (ns legacy.status-im.data-store.communities (:require [clojure.set :as set] - [clojure.walk :as walk] - [status-im.constants :as constants])) + [status-im.constants :as constants] + [utils.transforms :as transforms])) (defn <-revealed-accounts-rpc [accounts] @@ -22,19 +22,26 @@ (reduce #(assoc %1 (key-fn %2) (<-request-to-join-community-rpc %2)) {} requests)) (defn <-chats-rpc - [chats] - (reduce-kv (fn [acc k v] - (assoc acc - (name k) - (-> v - (assoc :token-gated? (:tokenGated v) - :can-post? (:canPost v) - :can-view? (:canView v) - :hide-if-permissions-not-met? (:hideIfPermissionsNotMet v)) - (dissoc :canPost :tokenGated :canView :hideIfPermissionsNotMet) - (update :members walk/stringify-keys)))) - {} - chats)) + "This transformation from RPC is optimized differently because there can be + thousands of members in all chats and we don't want to transform them from JS + to CLJS because they will only be used to list community members or community + chat members." + [chats-js] + (let [chat-key-fn (fn [k] + (case k + "tokenGated" :token-gated? + "canPost" :can-post? + "can-view" :can-view? + "hideIfPermissionsNotMet" :hide-if-permissions-not-met? + (keyword k))) + chat-val-fn (fn [k v] + (if (= "members" k) + v + (transforms/js->clj v)))] + (transforms/<-js-map + chats-js + {:val-fn (fn [_ v] + (transforms/<-js-map v {:key-fn chat-key-fn :val-fn chat-val-fn}))}))) (defn <-categories-rpc [categ] @@ -48,49 +55,59 @@ [token-permission] (= (:type token-permission) constants/community-token-permission-become-member)) +(defn- rename-community-key + [k] + (case k + "canRequestAccess" :can-request-access? + "canManageUsers" :can-manage-users? + "canDeleteMessageForEveryone" :can-delete-message-for-everyone? + ;; This flag is misleading based on its name alone + ;; because it should not be used to decide if the user + ;; is *allowed* to join. Allowance is based on token + ;; permissions. Still, the flag can be used to know + ;; whether or not the user will have to wait until an + ;; admin approves a join request. + "canJoin" :can-join? + "requestedToJoinAt" :requested-to-join-at + "isMember" :is-member? + "outroMessage" :outro-message + "adminSettings" :admin-settings + "tokenPermissions" :token-permissions + "communityTokensMetadata" :tokens-metadata + "introMessage" :intro-message + "muteTill" :muted-till + "lastOpenedAt" :last-opened-at + "joinedAt" :joined-at + (keyword k))) + (defn <-rpc - [c] - (-> c - (set/rename-keys - {:canRequestAccess :can-request-access? - :canManageUsers :can-manage-users? - :canDeleteMessageForEveryone :can-delete-message-for-everyone? - ;; This flag is misleading based on its name alone - ;; because it should not be used to decide if the user - ;; is *allowed* to join. Allowance is based on token - ;; permissions. Still, the flag can be used to know - ;; whether or not the user will have to wait until an - ;; admin approves a join request. - :canJoin :can-join? - :requestedToJoinAt :requested-to-join-at - :isMember :is-member? - :outroMessage :outro-message - :adminSettings :admin-settings - :tokenPermissions :token-permissions - :communityTokensMetadata :tokens-metadata - :introMessage :intro-message - :muteTill :muted-till - :lastOpenedAt :last-opened-at - :joinedAt :joined-at}) - (update :admin-settings - set/rename-keys - {:pinMessageAllMembersEnabled :pin-message-all-members-enabled?}) - (update :members walk/stringify-keys) - (update :chats <-chats-rpc) - (update :token-permissions seq) - (update :categories <-categories-rpc) - (assoc :role-permissions? - (->> c - :tokenPermissions - vals - (some role-permission?))) - (assoc :membership-permissions? - (->> c - :tokenPermissions - vals - (some membership-permission?))) - (assoc :token-images - (reduce (fn [acc {sym :symbol image :image}] - (assoc acc sym image)) - {} - (:communityTokensMetadata c))))) + [c-js] + (let [community (transforms/<-js-map + c-js + {:key-fn rename-community-key + :val-fn (fn [k v] + (case k + "members" v + "chats" (<-chats-rpc v) + (transforms/js->clj v)))})] + (-> community + (update :admin-settings + set/rename-keys + {:pinMessageAllMembersEnabled :pin-message-all-members-enabled?}) + (update :token-permissions seq) + (update :categories <-categories-rpc) + (assoc :role-permissions? + (->> community + :tokenPermissions + vals + (some role-permission?))) + (assoc :membership-permissions? + (->> community + :tokenPermissions + vals + (some membership-permission?))) + (assoc :token-images + (reduce (fn [acc {sym :symbol image :image}] + (assoc acc sym image)) + {} + (:communityTokensMetadata community)))))) diff --git a/src/legacy/status_im/ui/screens/communities/members.cljs b/src/legacy/status_im/ui/screens/communities/members.cljs index b62aff2c78..c7ccdf3cef 100644 --- a/src/legacy/status_im/ui/screens/communities/members.cljs +++ b/src/legacy/status_im/ui/screens/communities/members.cljs @@ -127,7 +127,7 @@ (when (and can-manage-users? (= constants/community-on-request-access (:access permissions))) [requests-to-join community-id]) [rn/flat-list - {:data (keys sorted-members) + {:data sorted-members :render-data {:community-id community-id :my-public-key my-public-key :can-kick-users? (and can-manage-users? diff --git a/src/native_module/core.cljs b/src/native_module/core.cljs index 6587d2efc9..f89e22d1e2 100644 --- a/src/native_module/core.cljs +++ b/src/native_module/core.cljs @@ -332,7 +332,9 @@ (defn set-blank-preview-flag [flag] (log/debug "[native-module] set-blank-preview-flag") - (.setBlankPreviewFlag ^js (encryption) flag)) + ;; Sometimes the app crashes during logout because `flag` is nil. + (when flag + (.setBlankPreviewFlag ^js (encryption) flag))) (defn get-device-model-info [] diff --git a/src/status_im/contexts/chat/messenger/messages/link_preview/events.cljs b/src/status_im/contexts/chat/messenger/messages/link_preview/events.cljs index 6ae71cb71f..f75d4a7172 100644 --- a/src/status_im/contexts/chat/messenger/messages/link_preview/events.cljs +++ b/src/status_im/contexts/chat/messenger/messages/link_preview/events.cljs @@ -1,7 +1,5 @@ (ns status-im.contexts.chat.messenger.messages.link-preview.events (:require [camel-snake-kebab.core :as csk] - [status-im.common.json-rpc.events :as json-rpc] - [taoensso.timbre :as log] [utils.collection] [utils.re-frame :as rf])) @@ -35,17 +33,6 @@ {:fx [[:dispatch [:profile.settings/profile-update :link-preview-request-enabled (boolean enabled?)]]]})) -(rf/reg-event-fx :chat.ui/link-preview-whitelist-received - (fn [{:keys [db]} [whitelist]] - {:db (assoc db :link-previews-whitelist whitelist)})) - -(rf/reg-fx :chat.ui/request-link-preview-whitelist - (fn [] - (json-rpc/call {:method "wakuext_getLinkPreviewWhitelist" - :params [] - :on-success [:chat.ui/link-preview-whitelist-received] - :on-error #(log/error "Failed to get link preview whitelist")}))) - (rf/reg-event-fx :chat.ui/enable-link-previews (fn [{{:profile/keys [profile]} :db} [site enabled?]] (let [enabled-sites (if enabled? diff --git a/src/status_im/contexts/chat/messenger/messages/transport/events.cljs b/src/status_im/contexts/chat/messenger/messages/transport/events.cljs index 1594ddbe10..3b59417737 100644 --- a/src/status_im/contexts/chat/messenger/messages/transport/events.cljs +++ b/src/status_im/contexts/chat/messenger/messages/transport/events.cljs @@ -103,11 +103,11 @@ (models.contact/process-js-contacts cofx response-js) (seq communities) - (let [communities-clj (types/js->clj communities)] + (do (js-delete response-js "communities") (rf/merge cofx (process-next response-js sync-handler) - (communities/handle-communities communities-clj))) + (communities/handle-communities communities))) (seq bookmarks) (let [bookmarks-clj (types/js->clj bookmarks)] diff --git a/src/status_im/contexts/communities/actions/channel_view_details/view.cljs b/src/status_im/contexts/communities/actions/channel_view_details/view.cljs index d70078262e..1312ff29f8 100644 --- a/src/status_im/contexts/communities/actions/channel_view_details/view.cljs +++ b/src/status_im/contexts/communities/actions/channel_view_details/view.cljs @@ -57,19 +57,24 @@ :index index}) (defn- members - [items theme] - [rn/section-list - {:key-fn :public-key - :content-container-style {:padding-bottom 20} - :get-item-layout get-item-layout - :content-inset-adjustment-behavior :never - :sections items - :sticky-section-headers-enabled false - :render-section-header-fn contact-list/contacts-section-header - :render-section-footer-fn footer - :render-data {:theme theme} - :render-fn contact-item - :scroll-event-throttle 32}]) + [community-id chat-id theme] + (let [online-members (rf/sub [:communities/chat-members-sorted community-id chat-id :online]) + offline-members (rf/sub [:communities/chat-members-sorted community-id chat-id :offline])] + [rn/section-list + {:key-fn :public-key + :content-container-style {:padding-bottom 20} + :get-item-layout get-item-layout + :content-inset-adjustment-behavior :never + :sections [{:title (i18n/label :t/online) + :data online-members} + {:title (i18n/label :t/offline) + :data offline-members}] + :sticky-section-headers-enabled false + :render-section-header-fn contact-list/contacts-section-header + :render-section-footer-fn footer + :render-data {:theme theme} + :render-fn contact-item + :scroll-event-throttle 32}])) (defn view [] @@ -78,8 +83,6 @@ {:keys [description chat-name emoji muted chat-type color] :as chat} (rf/sub [:chats/chat-by-id chat-id]) pins-count (rf/sub [:chats/pin-messages-count chat-id]) - items (rf/sub [:communities/sorted-community-members-section-list - community-id chat-id]) theme (quo.theme/use-theme)] (rn/use-mount (fn [] (rf/dispatch [:pin-message/load-pin-messages chat-id]))) @@ -133,4 +136,4 @@ (if muted (home.actions/unmute-chat-action chat-id) (home.actions/mute-chat-action chat-id chat-type muted)))}]}]]] - [members items theme]])) + [members community-id chat-id theme]])) diff --git a/src/status_im/contexts/communities/actions/invite_contacts/view.cljs b/src/status_im/contexts/communities/actions/invite_contacts/view.cljs index 1e6fb74ee4..4b4cb974e6 100644 --- a/src/status_im/contexts/communities/actions/invite_contacts/view.cljs +++ b/src/status_im/contexts/communities/actions/invite_contacts/view.cljs @@ -54,7 +54,7 @@ :as item}] (let [user-selected? (rf/sub [:is-contact-selected? public-key]) {:keys [id]} (rf/sub [:get-screen-params]) - community-members-keys (set (keys (rf/sub [:communities/community-members id]))) + community-members-keys (set (rf/sub [:communities/community-members id])) community-member? (boolean (community-members-keys public-key)) on-toggle (fn [] (when-not community-member? diff --git a/src/status_im/contexts/communities/events.cljs b/src/status_im/contexts/communities/events.cljs index 07414ab4b2..0925a669e1 100644 --- a/src/status_im/contexts/communities/events.cljs +++ b/src/status_im/contexts/communities/events.cljs @@ -39,17 +39,6 @@ (rf/reg-event-fx :communities/handle-community handle-community) -(schema/=> handle-community - [:=> - [:catn - [:cofx :schema.re-frame/cofx] - [:args - [:schema [:catn [:community-js map?]]]]] - [:maybe - [:map - [:db [:map [:communities map?]]] - [:fx vector?]]]]) - (rf/defn handle-removed-chats [{:keys [db]} chat-ids] {:db (reduce (fn [db chat-id] @@ -84,10 +73,9 @@ (rf/defn handle-communities {:events [:community/fetch-success]} - [{:keys [db]} communities] - {:fx - (->> communities - (map #(vector :dispatch [:communities/handle-community %])))}) + [{:keys [db]} communities-js] + {:fx (map (fn [c] [:dispatch [:communities/handle-community c]]) + communities-js)}) (rf/reg-event-fx :communities/request-to-join-result (fn [{:keys [db]} [community-id request-id response-js]] @@ -136,21 +124,30 @@ {} categories))})) +(rf/reg-event-fx :community/fetch-low-priority + (fn [] + {:fx [[:json-rpc/call + [{:method "wakuext_checkAndDeletePendingRequestToJoinCommunity" + :params [] + :js-response true + :on-success [:sanitize-messages-and-process-response] + :on-error #(log/info "failed to fetch communities" %)} + {:method "wakuext_collapsedCommunityCategories" + :params [] + :on-success [:communities/fetched-collapsed-categories-success] + :on-error #(log/error "failed to fetch collapsed community categories" %)}]]]})) + (rf/reg-event-fx :community/fetch (fn [_] - {:json-rpc/call [{:method "wakuext_serializedCommunities" - :params [] - :on-success #(rf/dispatch [:community/fetch-success %]) - :on-error #(log/error "failed to fetch communities" %)} - {:method "wakuext_checkAndDeletePendingRequestToJoinCommunity" - :params [] - :js-response true - :on-success #(rf/dispatch [:sanitize-messages-and-process-response %]) - :on-error #(log/info "failed to fetch communities" %)} - {:method "wakuext_collapsedCommunityCategories" - :params [] - :on-success #(rf/dispatch [:communities/fetched-collapsed-categories-success %]) - :on-error #(log/error "failed to fetch collapsed community categories" %)}]})) + {:fx [[:json-rpc/call + [{:method "wakuext_serializedCommunities" + :params [] + :on-success [:community/fetch-success] + :js-response true + :on-error #(log/error "failed to fetch communities" %)}]] + ;; Dispatch a little after 1000ms because other higher-priority events + ;; after login are being processed at the 1000ms mark. + [:dispatch-later [{:ms 1200 :dispatch [:community/fetch-low-priority]}]]]})) (defn update-previous-permission-addresses [{:keys [db]} [community-id]] diff --git a/src/status_im/contexts/communities/events_test.cljs b/src/status_im/contexts/communities/events_test.cljs index 4fb45e2435..f5392dfb40 100644 --- a/src/status_im/contexts/communities/events_test.cljs +++ b/src/status_im/contexts/communities/events_test.cljs @@ -151,7 +151,7 @@ (-> effects :json-rpc/call first (select-keys [:method :params])))))))) (deftest handle-community-test - (let [community {:id community-id :clock 2}] + (let [community #js {:id community-id :clock 2}] (testing "given a unjoined community" (let [effects (events/handle-community {} [community])] (is (match? community-id @@ -163,7 +163,7 @@ (filter some? (:fx effects)))))) (testing "given a joined community" - (let [community (assoc community :joined true) + (let [community #js {:id community-id :clock 2 :joined true} effects (events/handle-community {} [community])] (is (match? [[:dispatch @@ -172,16 +172,19 @@ (filter some? (:fx effects)))))) (testing "given a community with token-permissions-check" - (let [community (assoc community :token-permissions-check :fake-token-permissions-check) + (let [community #js + {:id community-id :clock 2 :token-permissions-check :fake-token-permissions-check} effects (events/handle-community {} [community])] (is (match? [[:dispatch [:communities/check-permissions-to-join-community-with-all-addresses community-id]]] (filter some? (:fx effects)))))) + (testing "given a community with lower clock" (let [effects (events/handle-community {:db {:communities {community-id {:clock 3}}}} [community])] (is (nil? effects)))) + (testing "given a community without clock" - (let [community (dissoc community :clock) + (let [community #js {:id community-id} effects (events/handle-community {} [community])] (is (nil? effects)))))) diff --git a/src/status_im/contexts/communities/home/view.cljs b/src/status_im/contexts/communities/home/view.cljs index 954c4d0343..e0573ae338 100644 --- a/src/status_im/contexts/communities/home/view.cljs +++ b/src/status_im/contexts/communities/home/view.cljs @@ -75,46 +75,51 @@ :banner (resources/get-image :discover) :accessibility-label :communities-home-discover-card}}) +(defn on-tab-change + [tab] + (rf/dispatch [:communities/select-tab tab])) + (defn view [] - (let [flat-list-ref (atom nil) - set-flat-list-ref #(reset! flat-list-ref %)] - (fn [] - (let [theme (quo.theme/use-theme) - customization-color (rf/sub [:profile/customization-color]) - selected-tab (or (rf/sub [:communities/selected-tab]) :joined) - {:keys [joined pending opened]} (rf/sub [:communities/grouped-by-status]) - selected-items (case selected-tab - :joined joined - :pending pending - :opened opened) - scroll-shared-value (reanimated/use-shared-value 0)] - [:<> - (if (empty? selected-items) - [common.empty-state/view - {:selected-tab selected-tab - :tab->content (empty-state-content theme)}] - [reanimated/flat-list - {:ref set-flat-list-ref - :key-fn :id - :content-inset-adjustment-behavior :never - :header [common.header-spacing/view] - :render-fn item-render - :style {:margin-top -1} - :data selected-items - :scroll-event-throttle 8 - :content-container-style {:padding-bottom - jump-to.constants/floating-shell-button-height} - :on-scroll #(common.banner/set-scroll-shared-value - {:scroll-input (oops/oget - % - "nativeEvent.contentOffset.y") - :shared-value scroll-shared-value})}]) - [:f> common.banner/animated-banner - {:content banner-data - :customization-color customization-color - :scroll-ref flat-list-ref - :tabs tabs-data - :selected-tab selected-tab - :on-tab-change (fn [tab] (rf/dispatch [:communities/select-tab tab])) - :scroll-shared-value scroll-shared-value}]])))) + (let [flat-list-ref (rn/use-ref-atom nil) + set-flat-list-ref (rn/use-callback #(reset! flat-list-ref %)) + theme (quo.theme/use-theme) + customization-color (rf/sub [:profile/customization-color]) + selected-tab (or (rf/sub [:communities/selected-tab]) :joined) + {:keys [joined pending opened]} (rf/sub [:communities/grouped-by-status]) + selected-items (case selected-tab + :joined joined + :pending pending + :opened opened) + scroll-shared-value (reanimated/use-shared-value 0) + on-scroll (rn/use-callback + (fn [event] + (common.banner/set-scroll-shared-value + {:scroll-input (oops/oget event + "nativeEvent.contentOffset.y") + :shared-value scroll-shared-value})))] + [:<> + (if (empty? selected-items) + [common.empty-state/view + {:selected-tab selected-tab + :tab->content (empty-state-content theme)}] + [reanimated/flat-list + {:ref set-flat-list-ref + :key-fn :id + :content-inset-adjustment-behavior :never + :header [common.header-spacing/view] + :render-fn item-render + :style {:margin-top -1} + :data selected-items + :scroll-event-throttle 8 + :content-container-style {:padding-bottom + jump-to.constants/floating-shell-button-height} + :on-scroll on-scroll}]) + [common.banner/animated-banner + {:content banner-data + :customization-color customization-color + :scroll-ref flat-list-ref + :tabs tabs-data + :selected-tab selected-tab + :on-tab-change on-tab-change + :scroll-shared-value scroll-shared-value}]])) diff --git a/src/status_im/contexts/profile/login/events.cljs b/src/status_im/contexts/profile/login/events.cljs index 477596b3a0..f73694defb 100644 --- a/src/status_im/contexts/profile/login/events.cljs +++ b/src/status_im/contexts/profile/login/events.cljs @@ -57,14 +57,15 @@ [{:method "wakuext_startMessenger" :on-success [:profile.login/messenger-started] :on-error #(log/error "failed to start messenger" %)}]] - [:dispatch [:universal-links/generate-profile-url]] [:dispatch [:community/fetch]] - [:push-notifications/load-preferences] - [:profile.config/get-node-config] + + ;; Wallet initialization can be delayed a little bit because we + ;; need to free the queue for heavier events first, such as + ;; loading chats and communities. This globally helps alleviate + ;; stuttering immediately after login. + [:dispatch-later [{:ms 500 :dispatch [:wallet/initialize]}]] + [:logs/set-level log-level] - [:activity-center.notifications/fetch-pending-contact-requests-fx] - [:activity-center/update-seen-state] - [:activity-center.notifications/fetch-unread-count] ;; Immediately try to open last chat. We can't wait until the ;; messenger has started and has processed all chats because @@ -90,24 +91,45 @@ ;; login phase 2: we want to load and show chats faster, so we split login into 2 phases (rf/reg-event-fx :profile.login/get-chats-callback (fn [{:keys [db]}] - (let [{:keys [notifications-enabled? key-uid - preview-privacy?]} (:profile/profile db)] + (let [{:keys [notifications-enabled? key-uid]} (:profile/profile db)] {:db db :fx [[:effects.profile/enable-local-notifications] [:contacts/initialize-contacts] - [:browser/initialize-browser] + ;; The delay is arbitrary. We just want to give some time for the + ;; thread to process more important events first, but we can't delay + ;; too much otherwise the UX may degrade due to stale data. + [:dispatch-later [{:ms 1500 :dispatch [:profile.login/non-critical-initialization]}]] [:dispatch [:mobile-network/on-network-status-change]] - [:group-chats/get-group-chat-invitations] [:profile.settings/get-profile-picture key-uid] - [:profile.settings/blank-preview-flag-changed preview-privacy?] - [:chat.ui/request-link-preview-whitelist] - [:visibility-status-updates/fetch] - [:switcher-cards/fetch] (when (ff/enabled? ::ff/wallet.wallet-connect) [:dispatch [:wallet-connect/init]]) (when notifications-enabled? [:effects/push-notifications-enable])]}))) +;; Login phase 3: events at this phase can wait a bit longer to be processed in +;; order to leave room for higher-priority or heavy weight events. +(rf/reg-event-fx :profile.login/non-critical-initialization + (fn [{:keys [db]}] + (let [{:keys [preview-privacy?]} (:profile/profile db)] + {:fx [[:browser/initialize-browser] + [:logging/initialize-web3-client-version] + [:group-chats/get-group-chat-invitations] + [:profile.settings/blank-preview-flag-changed preview-privacy?] + (when (ff/enabled? ::ff/shell.jump-to) + [:switcher-cards/fetch]) + [:visibility-status-updates/fetch] + [:dispatch [:universal-links/generate-profile-url]] + [:push-notifications/load-preferences] + [:profile.config/get-node-config] + [:activity-center.notifications/fetch-pending-contact-requests-fx] + [:activity-center/update-seen-state] + [:activity-center.notifications/fetch-unread-count] + [:pairing/get-our-installations] + [:json-rpc/call + [{:method "admin_nodeInfo" + :on-success [:profile.login/node-info-fetched] + :on-error #(log/error "node-info: failed error" %)}]]]}))) + (rf/reg-event-fx :profile.login/messenger-started (fn [{:keys [db]} [{:keys [mailservers]}]] (let [new-account? (get db :onboarding/new-account?)] @@ -119,11 +141,6 @@ (rf/dispatch [:chats-list/load-success result]) (rf/dispatch [:communities/get-user-requests-to-join]) (rf/dispatch [:profile.login/get-chats-callback]))}] - [:json-rpc/call - [{:method "admin_nodeInfo" - :on-success [:profile.login/node-info-fetched] - :on-error #(log/error "node-info: failed error" %)}]] - [:pairing/get-our-installations] (when-not new-account? [:dispatch [:universal-links/process-stored-event]])]}))) @@ -139,11 +156,9 @@ (if error {:db (update db :profile/login #(-> % (dissoc :processing) (assoc :error error)))} {:db (dissoc db :profile/login) - :fx [[:logging/initialize-web3-client-version] - (when (and new-account? (not recovered-account?)) - [:dispatch [:wallet-legacy/set-initial-blocks-range]]) - [:dispatch [:ens/update-usernames ensUsernames]] - [:dispatch [:wallet/initialize]] + :fx [(when (and new-account? (not recovered-account?)) + [:dispatch-later [{:ms 1000 :dispatch [:wallet-legacy/set-initial-blocks-range]}]]) + [:dispatch-later [{:ms 2000 :dispatch [:ens/update-usernames ensUsernames]}]] [:dispatch [:profile.login/login-existing-profile settings account]]]}))) (rf/reg-event-fx diff --git a/src/status_im/setup/dev.cljs b/src/status_im/setup/dev.cljs index 507e4590f1..1f89f62bf5 100644 --- a/src/status_im/setup/dev.cljs +++ b/src/status_im/setup/dev.cljs @@ -2,6 +2,7 @@ (:require ["react-native" :refer (DevSettings LogBox NativeModules)] [react-native.platform :as platform] + [status-im.setup.oops :as setup.oops] [status-im.setup.schema :as schema] [utils.re-frame :as rf])) @@ -47,6 +48,7 @@ :utils/dispatch-later :json-rpc/call}) (when ^:boolean js/goog.DEBUG + (setup.oops/setup!) (schema/setup!) (when platform/ios? ;; on Android this method doesn't work diff --git a/src/status_im/setup/oops.cljs b/src/status_im/setup/oops.cljs new file mode 100644 index 0000000000..f52062e664 --- /dev/null +++ b/src/status_im/setup/oops.cljs @@ -0,0 +1,20 @@ +(ns status-im.setup.oops + (:require [oops.config])) + +(defn setup! + "Change oops defaults to warn and print instead of throwing exceptions during + development." + [] + (oops.config/update-current-runtime-config! + merge + {:error-reporting :console + :expected-function-value :warn + :invalid-selector :warn + :missing-object-key :warn + :object-is-frozen :warn + :object-is-sealed :warn + :object-key-not-writable :warn + :unexpected-empty-selector :warn + :unexpected-object-value :warn + :unexpected-punching-selector :warn + :unexpected-soft-selector :warn})) diff --git a/src/status_im/setup/test_preload.cljs b/src/status_im/setup/test_preload.cljs index 2db545851c..31e02ae121 100644 --- a/src/status_im/setup/test_preload.cljs +++ b/src/status_im/setup/test_preload.cljs @@ -2,7 +2,8 @@ (:require ["bignumber.js" :as BigNumber] [matcher-combinators.core :as matcher-combinators] - [matcher-combinators.model :as matcher.model])) + [matcher-combinators.model :as matcher.model] + [status-im.setup.oops :as setup.oops])) ;; We must implement Matcher in order for tests to work with the `match?` ;; directive. @@ -16,3 +17,5 @@ :matcher-combinators.result/value actual} {:matcher-combinators.result/type :mismatch :matcher-combinators.result/value (matcher.model/->Mismatch this actual)}))) + +(setup.oops/setup!) diff --git a/src/status_im/subs/communities.cljs b/src/status_im/subs/communities.cljs index 5336cc418b..d43e043ed1 100644 --- a/src/status_im/subs/communities.cljs +++ b/src/status_im/subs/communities.cljs @@ -6,7 +6,9 @@ [re-frame.core :as re-frame] [status-im.constants :as constants] [status-im.contexts.communities.utils :as utils] + [status-im.contexts.profile.utils :as profile.utils] [status-im.subs.chat.utils :as subs.utils] + [status-im.subs.contact.utils :as contact.utils] [utils.i18n :as i18n] [utils.money :as money])) @@ -69,7 +71,7 @@ (fn [[_ community-id]] [(re-frame/subscribe [:communities/community community-id])]) (fn [[{:keys [members]}] _] - members)) + (js-keys members))) (re-frame/reg-sub :communities/community-chat-members @@ -92,51 +94,80 @@ {} public-keys)) -(defn- sort-members-by-name +(defn- sort-members-by-name-old [names descending? members] (if descending? - (sort-by #(get names (first %)) #(compare %2 %1) members) - (sort-by #(get names (first %)) members))) + (sort-by #(get names %) #(compare %2 %1) members) + (sort-by #(get names %) members))) -(re-frame/reg-sub - :communities/sorted-community-members +(defn- sort-members-by-name + [names members-keys] + (let [forced-last-key "zzzzzz" + sort-keyfn (fn [k] + (if-let [[primary-name secondary-name] (get names k)] + (or (some-> primary-name + string/lower-case) + (some-> secondary-name + string/lower-case)) + ;; Sort unknown keys at the end. + forced-last-key))] + (sort-by sort-keyfn members-keys))) + +;; This implementation is wrong, but since it's only used in a legacy view, we +;; can ignore it for now. +(re-frame/reg-sub :communities/sorted-community-members (fn [[_ community-id]] - (let [profile (re-frame/subscribe [:profile/profile]) - members (re-frame/subscribe [:communities/community-members community-id])] - [profile members])) + [(re-frame/subscribe [:profile/profile]) + (re-frame/subscribe [:communities/community-members community-id])]) (fn [[profile members] _] - (let [names (keys->names (keys members) profile)] + (let [names (keys->names members profile)] (->> members - (sort-members-by-name names false) + (sort-members-by-name-old names false) (sort-by #(visibility-status-utils/visibility-status-order (get % 0))))))) -(re-frame/reg-sub - :communities/sorted-community-members-section-list +(re-frame/reg-sub :communities/chat-members (fn [[_ community-id chat-id]] - (let [profile (re-frame/subscribe [:profile/profile]) - members (re-frame/subscribe [:communities/community-chat-members - community-id chat-id]) - visibility-status-updates (re-frame/subscribe - [:visibility-status-updates]) - my-status-update (re-frame/subscribe - [:multiaccount/current-user-visibility-status])] - [profile members visibility-status-updates my-status-update])) - (fn [[profile members visibility-status-updates my-status-update] _] - (let [online? (fn [public-key] - (let [{visibility-status-type :status-type} - (if (or (string/blank? (:public-key profile)) - (= (:public-key profile) public-key)) - my-status-update - (get visibility-status-updates public-key))] - (subs.utils/online? visibility-status-type))) - names (keys->names (keys members) profile)] - (->> members - (sort-members-by-name names true) - keys - (group-by online?) - (map (fn [[k v]] - {:title (if k (i18n/label :t/online) (i18n/label :t/offline)) - :data v})))))) + [(re-frame/subscribe [:profile/public-key]) + (re-frame/subscribe [:communities/community-chat-members community-id chat-id]) + (re-frame/subscribe [:visibility-status-updates]) + (re-frame/subscribe [:multiaccount/current-user-visibility-status])]) + (fn [[profile-pub-key members-js visibility-status-updates my-status-update] [_ _ _ visibility-status]] + (let [members-keys (js-keys members-js) + online? (fn [public-key] + (let [{visibility-status-type :status-type} + (if (or (string/blank? profile-pub-key) + (= profile-pub-key public-key)) + my-status-update + (get visibility-status-updates public-key))] + (subs.utils/online? visibility-status-type)))] + (filter (if (= :online visibility-status) + online? + (complement online?)) + members-keys)))) + +(defn- names-by-key + [contacts profile public-keys] + (let [names (reduce (fn [acc k] + (if-let [contact (get contacts k)] + (assoc acc k (contact.utils/contact-two-names contact profile)) + acc)) + {} + public-keys)] + (assoc names + (:public-key profile) + [(profile.utils/displayed-name profile) nil]))) + +;; This is a potentially expensive subscription because we don't control how +;; many members and contacts exist in the app-db. Future improvements include +;; removing members from the payload and paginating them from status-go. +(re-frame/reg-sub :communities/chat-members-sorted + (fn [[_ community-id chat-id visibility-status]] + [(re-frame/subscribe [:profile/profile]) + (re-frame/subscribe [:contacts/contacts-raw]) + (re-frame/subscribe [:communities/chat-members community-id chat-id visibility-status])]) + (fn [[profile contacts ^js members-keys]] + (sort-members-by-name (names-by-key contacts profile members-keys) + members-keys))) (re-frame/reg-sub :communities/featured-contract-communities @@ -177,6 +208,13 @@ (if (or (empty? @memo-communities-stack-items) (= view-id :communities-stack)) (let [grouped-communities (->> communities vals + ;; Remove data that can grow fast or is + ;; reliably not needed to list communities. + ;; We could use an allowlist of keys for + ;; optimal performance of this sub, but + ;; that's harder to maintain in case we miss + ;; any key. + (map #(dissoc % :members :chats :token-permissions :tokens-metadata)) (group-by #(group-communities-by-status requests %)) merge-opened-communities (map (fn [[k v]] diff --git a/src/status_im/subs/communities_test.cljs b/src/status_im/subs/communities_test.cljs index 9bae83729d..8e3a5e8cc9 100644 --- a/src/status_im/subs/communities_test.cljs +++ b/src/status_im/subs/communities_test.cljs @@ -11,6 +11,8 @@ [utils.re-frame :as rf])) (def community-id "0x02b5bdaf5a25fcfe2ee14c501fab1836b8de57f61621080c3d52073d16de0d98d6") +(def channel-id "0x1-channel-id") +(def chat-id (str community-id channel-id)) (h/deftest-sub :communities [sub-name] @@ -457,65 +459,117 @@ (match? [] (rf/sub [sub-name community-id])))))) -(h/deftest-sub :communities/sorted-community-members-section-list +(h/deftest-sub :communities/chat-members-sorted [sub-name] - (testing "returns sorted community members per online status" - (let [token-image-eth "data:image/jpeg;base64,/9j/2w" - channel-id-1 "89f98a1e-6776-4e5f-8626-8ab9f855253f" - channel-id-2 "a076358e-4638-470e-a3fb-584d0a542ce6" - chat-id-1 (str community-id channel-id-1) - chat-id-2 (str community-id channel-id-2) - community {:id community-id - :permissions {:access 3} - :token-images {"ETH" token-image-eth} - :name "Community super name" - :chats {channel-id-1 - {:description "x" - :emoji "🎲" - :permissions {:access 1} - :color "#88B0FF" - :name "random" - :categoryID "0c3c64e7-d56e-439b-a3fb-a946d83cb056" - :id channel-id-1 - :position 4 - :can-post? false - :members nil} - channel-id-2 - {:description "General channel for the community" - :emoji "🥔" - :permissions {:access 1} - :color "#4360DF" - :name "general" - :categoryID "0c3c64e7-d56e-439b-a3fb-a946d83cb056" - :id channel-id-2 - :position 0 - :token-gated? true - :can-post? false - :members {"0x01" {"roles" [1]} - "0x02" {"roles" [1]} - "0x05" {"roles" [1]}}}} - :members {"0x01" {"roles" [1]} - "0x02" {"roles" [1]} - "0x03" {"roles" [1]} - "0x04" {"roles" [1]}} - :can-request-access? false - :outroMessage "bla" - :verified false}] + (let [token-image-eth "data:image/jpeg;base64,/9j/2w" + channel-id-1 "89f98a1e-6776-4e5f-8626-8ab9f855253f" + channel-id-2 "a076358e-4638-470e-a3fb-584d0a542ce6" + chat-id-2 (str community-id channel-id-2) + + member-id-1 "0x01" + member-id-2 "0x02" + + visibility-status-updates + {member-id-1 {:status-type constants/visibility-status-always-online} + member-id-2 {:status-type constants/visibility-status-always-online}} + + contacts + {member-id-1 {:display-name "John Marston"} + member-id-2 {:display-name "Arthur Morgan"}} + + community {:id community-id + :permissions {:access 3} + :token-images {"ETH" token-image-eth} + :name "Community super name" + :chats {channel-id-1 + {:description "x" + :emoji "🎲" + :permissions {:access 1} + :color "#88B0FF" + :name "random" + :categoryID "0c3c64e7-d56e-439b-a3fb-a946d83cb056" + :id channel-id-1 + :position 4 + :can-post? false + :members nil} + channel-id-2 + {:description "General channel for the community" + :emoji "🥔" + :permissions {:access 1} + :color "#4360DF" + :name "general" + :categoryID "0c3c64e7-d56e-439b-a3fb-a946d83cb056" + :id channel-id-2 + :position 0 + :token-gated? true + :can-post? false + :members (clj->js {member-id-1 {"roles" [1]} + member-id-2 {"roles" [1]} + "0x05" {"roles" [1]}})}} + :members (js->clj {member-id-1 {"roles" [1]} + member-id-2 {"roles" [1]} + "0x03" {"roles" [1]} + "0x04" {"roles" [1]}})}] + (testing "returns sorted community members who are online" + (swap! rf-db/app-db assoc :contacts/contacts contacts) (swap! rf-db/app-db assoc-in [:communities community-id] community) (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc :visibility-status-updates visibility-status-updates) + (is (= [member-id-2 member-id-1] + (rf/sub [sub-name community-id chat-id-2 :online])))) + + (testing "returns sorted community members per offline status" + (swap! rf-db/app-db assoc-in [:communities community-id] community) + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc :visibility-status-updates visibility-status-updates) + (is (= ["0x05"] (rf/sub [sub-name community-id chat-id-2 :offline])))))) + +(h/deftest-sub :communities/chat-members + [sub-name] + (let [member-1-id "0x1-member" + member-2-id "0x2-member" + + visibility-status-updates + {member-1-id {:status-type constants/visibility-status-always-online} + member-2-id {:status-type constants/visibility-status-always-online}} + + communities + {community-id {:id community-id + :chats {channel-id {:token-gated? false + :members (clj->js {member-2-id {}})}} + :members (clj->js {member-1-id {} + member-2-id {}})}}] + (testing "members from non token-gated channels and online" + (swap! rf-db/app-db assoc :visibility-status-updates visibility-status-updates) + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc :communities communities) + + ;; When channel is not token-gated, all community members are considered. + (is (= [member-1-id member-2-id] + (rf/sub [sub-name community-id chat-id :online])))) + + (testing "members from token-gated channels and online" + (swap! rf-db/app-db assoc :visibility-status-updates visibility-status-updates) + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) (swap! rf-db/app-db assoc - :visibility-status-updates - {"0x01" {:status-type constants/visibility-status-always-online} - "0x02" {:status-type constants/visibility-status-always-online}}) - (testing "a non-token gated community should look at all members of a community" - (is (= [{:title (i18n/label :t/online) - :data ["0x01" "0x02"]} - {:title (i18n/label :t/offline) - :data ["0x03" "0x04"]}] - (rf/sub [sub-name community-id chat-id-1])))) - (testing "a token gated community should use the members option in the channel" - (is (= [{:title (i18n/label :t/online) - :data ["0x01" "0x02"]} - {:title (i18n/label :t/offline) - :data ["0x05"]}] - (rf/sub [sub-name community-id chat-id-2]))))))) + :communities + (assoc-in communities [community-id :chats channel-id :token-gated?] true)) + + ;; When channel is token-gated, only its members are considered. + (is (= [member-2-id] + (rf/sub [sub-name community-id chat-id :online])))) + + (testing "members from token-gated channels and offline" + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc + :communities + (assoc-in communities [community-id :chats channel-id :token-gated?] true)) + + (is (= [member-2-id] (rf/sub [sub-name community-id chat-id :offline])))) + + (testing "members from non token-gated channels and offline" + (swap! rf-db/app-db assoc :profile/profile profile-test/sample-profile) + (swap! rf-db/app-db assoc :communities communities) + + (is (= [member-1-id member-2-id] + (rf/sub [sub-name community-id chat-id :offline])))))) diff --git a/src/status_im/subs/contact.cljs b/src/status_im/subs/contact.cljs index 2b5ff555a0..7c9c684cd4 100644 --- a/src/status_im/subs/contact.cljs +++ b/src/status_im/subs/contact.cljs @@ -2,29 +2,15 @@ (:require [clojure.set :as set] [clojure.string :as string] - [legacy.status-im.ui.screens.profile.visibility-status.utils :as visibility-status-utils] [quo.theme] [re-frame.core :as re-frame] [status-im.constants :as constants] - [status-im.contexts.profile.utils :as profile.utils] [status-im.subs.chat.utils :as chat.utils] [status-im.subs.contact.utils :as contact.utils] [utils.address :as address] [utils.collection] [utils.i18n :as i18n])) -(defn query-chat-contacts - [{:keys [contacts]} all-contacts query-fn] - (let [participant-set (into #{} (filter identity) contacts)] - (query-fn (comp participant-set :public-key) (vals all-contacts)))) - -(re-frame/reg-sub - ::query-current-chat-contacts - :<- [:chats/current-chat] - :<- [:contacts/contacts] - (fn [[chat contacts] [_ query-fn]] - (query-chat-contacts chat contacts query-fn))) - (re-frame/reg-sub :multiaccount/profile-pictures-show-to :<- [:profile/profile] @@ -124,15 +110,6 @@ sort vals))) -(re-frame/reg-sub - :contacts/sorted-contacts - :<- [:contacts/active] - (fn [active-contacts] - (->> active-contacts - (sort-by :primary-name) - (sort-by - #(visibility-status-utils/visibility-status-order (:public-key %)))))) - (re-frame/reg-sub :contacts/sorted-and-grouped-by-first-letter :<- [:contacts/active] @@ -150,12 +127,6 @@ {:title title :data data}))))) -(re-frame/reg-sub - :contacts/active-count - :<- [:contacts/active] - (fn [active-contacts] - (count active-contacts))) - (re-frame/reg-sub :contacts/blocked :<- [:contacts/contacts] @@ -171,12 +142,6 @@ (fn [contacts] (into #{} (map :public-key contacts)))) -(re-frame/reg-sub - :contacts/blocked-count - :<- [:contacts/blocked] - (fn [blocked-contacts] - (count blocked-contacts))) - (defn public-key-and-ens-name->new-contact [public-key ens-name] (let [contact {:public-key public-key}] @@ -226,24 +191,8 @@ (fn [[_ contact-identity] _] [(re-frame/subscribe [:contacts/contact-by-identity contact-identity]) (re-frame/subscribe [:profile/profile])]) - (fn [[{:keys [primary-name] :as contact} - {:keys [public-key preferred-name display-name name]}] - [_ contact-identity]] - [(if (= public-key contact-identity) - (cond - (not (string/blank? preferred-name)) preferred-name - (not (string/blank? display-name)) display-name - (not (string/blank? primary-name)) primary-name - (not (string/blank? name)) name - :else public-key) - (profile.utils/displayed-name contact)) - (:secondary-name contact)])) - -(re-frame/reg-sub - :contacts/all-contacts-not-in-current-chat - :<- [::query-current-chat-contacts remove] - (fn [contacts] - (filter :added? contacts))) + (fn [[contact profile] [_ _]] + (contact.utils/contact-two-names contact profile))) (defn get-all-contacts-in-group-chat [members admins contacts {:keys [public-key preferred-name name display-name] :as current-account}] @@ -300,8 +249,10 @@ :<- [:multiaccount/contact] (fn [[contacts multiaccount] [_ address]] (if (address/address= address (:public-key multiaccount)) - multiaccount - (find-contact-by-address contacts address)))) + (merge (contact.utils/build-contact-from-public-key address) + multiaccount) + (or (find-contact-by-address contacts address) + (contact.utils/build-contact-from-public-key address))))) (re-frame/reg-sub :contacts/contact-customization-color-by-address diff --git a/src/status_im/subs/contact/utils.cljs b/src/status_im/subs/contact/utils.cljs index 37862bea26..b6778a2a58 100644 --- a/src/status_im/subs/contact/utils.cljs +++ b/src/status_im/subs/contact/utils.cljs @@ -1,8 +1,10 @@ (ns status-im.subs.contact.utils (:require + [clojure.string :as string] [native-module.core :as native-module] [status-im.common.pixel-ratio :as pixel-ratio] [status-im.constants :as constants] + [status-im.contexts.profile.utils :as profile.utils] [utils.address :as address])) (defn replace-contact-image-uri @@ -44,11 +46,29 @@ (assoc contact :images images))) - -(defn build-contact-from-public-key +(defn- build-contact-from-public-key* [public-key] (when public-key (let [compressed-key (native-module/serialize-legacy-key public-key)] {:public-key public-key :compressed-key compressed-key :primary-name (address/get-shortened-compressed-key (or compressed-key public-key))}))) + +(def build-contact-from-public-key + "The result of this function is stable because it relies exclusively on the + public key, but it's not cheap to be performed hundreds of times in a row, + such as when displaying a long list of channel members." + (memoize build-contact-from-public-key*)) + +(defn contact-two-names + [{:keys [primary-name] :as contact} + {:keys [public-key preferred-name display-name name]}] + [(if (= public-key (:public-key contact)) + (cond + (not (string/blank? preferred-name)) preferred-name + (not (string/blank? display-name)) display-name + (not (string/blank? primary-name)) primary-name + (not (string/blank? name)) name + :else public-key) + (profile.utils/displayed-name contact)) + (:secondary-name contact)]) diff --git a/src/status_im/subs/contact_test.cljs b/src/status_im/subs/contact_test.cljs index f0111ba253..59e3090704 100644 --- a/src/status_im/subs/contact_test.cljs +++ b/src/status_im/subs/contact_test.cljs @@ -187,7 +187,7 @@ :preferred-name "Preferred Name"} :contacts/contacts - {profile-key {:primary-name "Primary Name"} + {profile-key {:primary-name "Primary Name" :public-key profile-key} "contact-key" {:secondary-name "Secondary Name"}}) (is (= ["Preferred Name" nil] (rf/sub [sub-name profile-key]))) diff --git a/src/utils/transforms.cljs b/src/utils/transforms.cljs index 6adb4e3a5a..5685e342e4 100644 --- a/src/utils/transforms.cljs +++ b/src/utils/transforms.cljs @@ -15,6 +15,34 @@ (defn clj->json [data] (clj->pretty-json data 0)) +(defn <-js-map + "Shallowly transforms JS Object keys/values with `key-fn`/`val-fn`. + + Returns nil if `m` is not an instance of `js/Object`. + + Implementation taken from `js->clj`, but with the ability to customize how + keys and/or values are transformed in one loop. + + This function is useful when you don't want to recursively apply the same + transformation to keys/values. For example, many maps in the app-db are + indexed by ID, like `community.members`. If we convert the entire community + with (js->clj m :keywordize-keys true), then IDs will be converted to + keywords, but we want them as strings. Instead of transforming to keywords and + then transforming back to strings, it's better to not transform them at all. + " + ([^js m] + (<-js-map m nil)) + ([^js m {:keys [key-fn val-fn]}] + (when (identical? (type m) js/Object) + (persistent! + (reduce (fn [r k] + (let [v (oops/oget+ m k) + new-key (if key-fn (key-fn k v) k) + new-val (if val-fn (val-fn k v) v)] + (assoc! r new-key new-val))) + (transient {}) + (js-keys m)))))) + (defn js-stringify [js-object spaces] (.stringify js/JSON js-object nil spaces)) diff --git a/src/utils/transforms_test.cljs b/src/utils/transforms_test.cljs new file mode 100644 index 0000000000..4b6972f2a3 --- /dev/null +++ b/src/utils/transforms_test.cljs @@ -0,0 +1,37 @@ +(ns utils.transforms-test + (:require + [cljs.test :refer [are deftest is testing]] + [utils.transforms :as sut])) + +(defn equals-as-json + [m1 m2] + (= (js/JSON.stringify (clj->js m1)) + (js/JSON.stringify (clj->js m2)))) + +(deftest <-js-map-test + (testing "without transforming keys/values" + (are [expected m] + (is (equals-as-json expected (sut/<-js-map m))) + nil nil + nil #js [] + #js {} #js {} + #js {"a" 1 "b" 2} #js {"a" 1 "b" 2})) + + (testing "with key/value transformation" + (is (equals-as-json {"aa" [1] "bb" [2]} + (sut/<-js-map #js {"a" 1 "b" 2} + {:key-fn (fn [k] (str k k)) + :val-fn (fn [_ v] (vector v))})))) + + (testing "it is non-recursive" + (is (equals-as-json {:a 1 :b #js {"c" 3}} + (sut/<-js-map #js {"a" 1 "b" #js {"c" 3}} + {:key-fn (fn [k] (keyword k))})))) + + (testing "value transformation based on the key" + (is (equals-as-json {"a" 1 "b" "banana"} + (sut/<-js-map #js {"a" 1 "b" 2} + {:val-fn (fn [k v] + (if (= "b" k) + "banana" + v))})))))