Pinned messages in chats

Signed-off-by: Brian Sztamfater <brian@status.im>
This commit is contained in:
Brian Sztamfater 2021-04-02 11:44:39 -03:00 committed by Brian Sztamfater
parent 440e6d2047
commit 089f42e0e9
No known key found for this signature in database
GPG Key ID: 59EB921E0706B48F
25 changed files with 723 additions and 136 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -232,6 +232,14 @@
{:db (assoc-in db [:chats chat-id] chat) {:db (assoc-in db [:chats chat-id] chat)
:dispatch [:chat.ui/navigate-to-chat chat-id]})) :dispatch [:chat.ui/navigate-to-chat chat-id]}))
(fx/defn navigate-to-user-pinned-messages
"Takes coeffects map and chat-id, returns effects necessary for navigation and preloading data"
{:events [:chat.ui/navigate-to-pinned-messages]}
[{db :db :as cofx} chat-id]
(fx/merge cofx
{:db (assoc db :current-chat-id chat-id)}
(navigation/navigate-to :chat-pinned-messages nil)))
(fx/defn start-chat (fx/defn start-chat
"Start a chat, making sure it exists" "Start a chat, making sure it exists"
{:events [:chat.ui/start-chat]} {:events [:chat.ui/start-chat]}

View File

@ -7,7 +7,8 @@
[status-im.chat.models.message-list :as message-list] [status-im.chat.models.message-list :as message-list]
[taoensso.timbre :as log] [taoensso.timbre :as log]
[status-im.ethereum.json-rpc :as json-rpc] [status-im.ethereum.json-rpc :as json-rpc]
[clojure.string :as string])) [clojure.string :as string]
[status-im.chat.models.pin-message :as models.pin-message]))
(defn cursor->clock-value (defn cursor->clock-value
[^js cursor] [^js cursor]
@ -115,7 +116,8 @@
(when (or first-request cursor) (when (or first-request cursor)
(merge (merge
{:db (assoc-in db [:pagination-info chat-id :loading-messages?] true)} {:db (assoc-in db [:pagination-info chat-id :loading-messages?] true)}
{:utils/dispatch-later [{:ms 100 :dispatch [:load-more-reactions cursor chat-id]}]} {:utils/dispatch-later [{:ms 100 :dispatch [:load-more-reactions cursor chat-id]}
{:ms 100 :dispatch [::models.pin-message/load-pin-messages chat-id]}]}
(data-store.messages/messages-by-chat-id-rpc (data-store.messages/messages-by-chat-id-rpc
chat-id chat-id
cursor cursor

View File

@ -0,0 +1,94 @@
(ns status-im.chat.models.pin-message
(:require [status-im.chat.models.message-list :as message-list]
[status-im.constants :as constants]
[status-im.data-store.pin-messages :as data-store.pin-messages]
[status-im.utils.fx :as fx]
[taoensso.timbre :as log]
[re-frame.core :as re-frame]))
(fx/defn handle-failed-loading-pin-messages
{:events [::failed-loading-pin-messages]}
[{:keys [db]} current-chat-id _ err]
(log/error "failed loading pin messages" current-chat-id err)
(when current-chat-id
{:db (assoc-in db [:pagination-info current-chat-id :loading-pin-messages?] false)}))
(fx/defn pin-messages-loaded
{:events [::pin-messages-loaded]}
[{db :db} chat-id {:keys [cursor pinned-messages]}]
(let [all-messages (reduce (fn [acc {:keys [message-id] :as message}]
(assoc acc message-id message))
{}
pinned-messages)
messages-id-list (map :message-id pinned-messages)]
{:db (-> db
(assoc-in [:pagination-info chat-id :loading-pin-messages?] false)
(assoc-in [:pin-messages chat-id] all-messages)
(assoc-in [:pin-message-lists chat-id] (message-list/add-many nil (vals all-messages)))
(assoc-in [:pagination-info chat-id :all-pin-loaded?]
(empty? cursor)))}))
(fx/defn receive-signal
[{:keys [db] :as cofx} pin-messages]
(let [{:keys [chat-id]} (first pin-messages)]
(when (= chat-id (db :current-chat-id))
(let [{:keys [chat-id]} (first pin-messages)
already-loaded-pin-messages (get-in db [:pin-messages chat-id] {})
already-loaded-messages (get-in db [:messages chat-id] {})
all-messages (reduce (fn [acc {:keys [message_id pinned from]}]
;; Add to or remove from pinned message list, and normalizing pinned-by property
(let [current-message (get already-loaded-messages message_id)
current-message-pin (merge current-message
{:pinned pinned
:pinned-by from})]
(cond-> acc
(nil? pinned)
(dissoc message_id)
(and (some? pinned) (some? current-message))
(assoc message_id current-message-pin))))
already-loaded-pin-messages
pin-messages)]
{:db (-> db
(assoc-in [:pin-messages chat-id] all-messages)
(assoc-in [:pin-message-lists chat-id]
(message-list/add-many nil (vals all-messages))))}))))
(fx/defn load-more-pin-messages
[{:keys [db]} chat-id first-request]
(let [not-all-loaded? (not (get-in db [:pagination-info chat-id :all-loaded?]))
not-loading-pin-messages? (not (get-in db [:pagination-info chat-id :loading-pin-messages?]))]
(when not-loading-pin-messages?
(fx/merge
{:db (assoc-in db [:pagination-info chat-id :loading-pin-messages?] true)}
(data-store.pin-messages/pinned-message-by-chat-id-rpc
chat-id
nil
constants/default-number-of-pin-messages
#(re-frame/dispatch [::pin-messages-loaded chat-id %])
#(re-frame/dispatch [::failed-loading-pin-messages chat-id %]))))))
(fx/defn send-pin-message
"Pin message, rebuild pinned messages list"
{:events [::send-pin-message]}
[{:keys [db] :as cofx} {:keys [chat-id message-id pinned] :as pin-message}]
(let [current-public-key (get-in db [:multiaccount :public-key])
message (merge pin-message {:pinned-by current-public-key})]
(fx/merge cofx
{:db (cond-> db
pinned
(->
(update-in [:pin-message-lists chat-id] message-list/add message)
(assoc-in [:pin-messages chat-id message-id] message))
(not pinned)
(->
(update-in [:pin-message-lists chat-id] message-list/remove-message pin-message)
(update-in [:pin-messages chat-id] dissoc message-id)))}
(data-store.pin-messages/send-pin-message {:chat-id (pin-message :chat-id)
:message_id (pin-message :message-id)
:pinned (pin-message :pinned)}))))
(fx/defn load-pin-messages
{:events [::load-pin-messages]}
[{:keys [db] :as cofx} chat-id]
(load-more-pin-messages cofx chat-id true))

View File

@ -66,6 +66,7 @@
(def ^:const min-password-length 6) (def ^:const min-password-length 6)
(def ^:const max-group-chat-participants 20) (def ^:const max-group-chat-participants 20)
(def ^:const default-number-of-messages 20) (def ^:const default-number-of-messages 20)
(def ^:const default-number-of-pin-messages 3)
(def ^:const mailserver-password "status-offline-inbox") (def ^:const mailserver-password "status-offline-inbox")

View File

@ -25,4 +25,9 @@
#(when new-contact? #(when new-contact?
(navigation/navigate-back %)) (navigation/navigate-back %))
#(when ens-name #(when ens-name
(contact/name-verified % public-key ens-name))))) (contact/name-verified % public-key ens-name)))))
(fx/defn pinned-messages-pressed
{:events [:contact.ui/pinned-messages-pressed]}
[cofx public-key]
(chat/navigate-to-user-pinned-messages cofx public-key))

View File

@ -10,7 +10,8 @@
(assoc :text (:text content) (assoc :text (:text content)
:sticker (:sticker content)) :sticker (:sticker content))
:always :always
(clojure.set/rename-keys {:chat-id :chatId (clojure.set/rename-keys {:chat-id :chat_id
:whisper-timestamp :whisperTimestamp
:community-id :communityId :community-id :communityId
:clock-value :clock}))) :clock-value :clock})))

View File

@ -0,0 +1,31 @@
(ns status-im.data-store.pin-messages
(:require [clojure.set :as clojure.set]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.utils.fx :as fx]
[taoensso.timbre :as log]
[status-im.data-store.messages :as messages]))
(defn <-rpc [message]
(-> message
(merge (messages/<-rpc (message :message)))
(clojure.set/rename-keys {:pinnedAt :pinned-at
:pinnedBy :pinned-by})
(dissoc :message)))
(defn pinned-message-by-chat-id-rpc [chat-id
cursor
limit
on-success
on-failure]
{::json-rpc/call [{:method (json-rpc/call-ext-method "chatPinnedMessages")
:params [chat-id cursor limit]
:on-success (fn [result]
(let [result (clojure.set/rename-keys result {:pinnedMessages :pinned-messages})]
(on-success (update result :pinned-messages #(map <-rpc %)))))
:on-failure on-failure}]})
(fx/defn send-pin-message [cofx pin-message]
{::json-rpc/call [{:method (json-rpc/call-ext-method "sendPinMessage")
:params [(messages/->rpc pin-message)]
:on-success #(log/debug "successfully pinned message" pin-message)
:on-failure #(log/error "failed to pin message" % pin-message)}]})

View File

@ -96,6 +96,8 @@
"wakuext_getLinkPreviewData" {} "wakuext_getLinkPreviewData" {}
"wakuext_requestCommunityInfoFromMailserver" {} "wakuext_requestCommunityInfoFromMailserver" {}
"wakuext_deactivateChat" {} "wakuext_deactivateChat" {}
"wakuext_sendPinMessage" {}
"wakuext_chatPinnedMessages" {}
;;TODO not used anywhere? ;;TODO not used anywhere?
"wakuext_deleteChat" {} "wakuext_deleteChat" {}
"wakuext_saveContact" {} "wakuext_saveContact" {}

View File

@ -195,6 +195,8 @@
(reg-root-key-sub ::reactions :reactions) (reg-root-key-sub ::reactions :reactions)
(reg-root-key-sub ::message-lists :message-lists) (reg-root-key-sub ::message-lists :message-lists)
(reg-root-key-sub ::pagination-info :pagination-info) (reg-root-key-sub ::pagination-info :pagination-info)
(reg-root-key-sub ::pin-message-lists :pin-message-lists)
(reg-root-key-sub ::pin-messages :pin-messages)
(reg-root-key-sub :tos-accept-next-root :tos-accept-next-root) (reg-root-key-sub :tos-accept-next-root :tos-accept-next-root)
@ -874,7 +876,7 @@
:chats/current-chat-chat-view :chats/current-chat-chat-view
:<- [:chats/current-chat] :<- [:chats/current-chat]
(fn [current-chat] (fn [current-chat]
(select-keys current-chat [:chat-id :show-input? :group-chat :admins :invitation-admin :public? :chat-type :color :chat-name :synced-to :synced-from]))) (select-keys current-chat [:chat-id :show-input? :group-chat :admins :invitation-admin :public? :chat-type :color :chat-name :synced-to :synced-from :community-id])))
(re-frame/reg-sub (re-frame/reg-sub
:current-chat/metadata :current-chat/metadata
@ -910,6 +912,12 @@
(fn [messages [_ chat-id]] (fn [messages [_ chat-id]]
(get messages chat-id {}))) (get messages chat-id {})))
(re-frame/reg-sub
:chats/pinned
:<- [::pin-messages]
(fn [pin-messages [_ chat-id]]
(get pin-messages chat-id {})))
(re-frame/reg-sub (re-frame/reg-sub
:chats/message-reactions :chats/message-reactions
:<- [:multiaccount/public-key] :<- [:multiaccount/public-key]
@ -939,6 +947,12 @@
(fn [pagination-info [_ chat-id]] (fn [pagination-info [_ chat-id]]
(get-in pagination-info [chat-id :loading-messages?]))) (get-in pagination-info [chat-id :loading-messages?])))
(re-frame/reg-sub
:chats/loading-pin-messages?
:<- [::pagination-info]
(fn [pagination-info [_ chat-id]]
(get-in pagination-info [chat-id :loading-pin-messages?])))
(re-frame/reg-sub (re-frame/reg-sub
:chats/public? :chats/public?
:<- [::chats] :<- [::chats]
@ -951,14 +965,25 @@
(fn [message-lists [_ chat-id]] (fn [message-lists [_ chat-id]]
(get message-lists chat-id))) (get message-lists chat-id)))
(re-frame/reg-sub
:chats/pin-message-list
:<- [::pin-message-lists]
(fn [pin-message-lists [_ chat-id]]
(get pin-message-lists chat-id)))
(defn hydrate-messages (defn hydrate-messages
"Pull data from messages and add it to the sorted list" "Pull data from messages and add it to the sorted list"
[message-list messages] ([message-list messages] (hydrate-messages message-list messages {}))
(keep #(if (= :message (% :type)) ([message-list messages pinned-messages]
(when-let [message (messages (% :message-id))] (keep #(if (= :message (% :type))
(merge message %)) (when-let [message (messages (% :message-id))]
%) (let [pinned-message (get pinned-messages (% :message-id))
message-list)) pinned (if pinned-message true (some? (message :pinned-by)))
pinned-by (when pinned (or (message :pinned-by) (pinned-message :pinned-by)))
message (assoc message :pinned pinned :pinned-by pinned-by)]
(merge message %)))
%)
message-list)))
(re-frame/reg-sub (re-frame/reg-sub
:chats/chat-no-messages? :chats/chat-no-messages?
@ -972,11 +997,12 @@
(fn [[_ chat-id] _] (fn [[_ chat-id] _]
[(re-frame/subscribe [:chats/message-list chat-id]) [(re-frame/subscribe [:chats/message-list chat-id])
(re-frame/subscribe [:chats/chat-messages chat-id]) (re-frame/subscribe [:chats/chat-messages chat-id])
(re-frame/subscribe [:chats/pinned chat-id])
(re-frame/subscribe [:chats/loading-messages? chat-id]) (re-frame/subscribe [:chats/loading-messages? chat-id])
(re-frame/subscribe [:chats/synced-from chat-id]) (re-frame/subscribe [:chats/synced-from chat-id])
(re-frame/subscribe [:chats/chat-type chat-id]) (re-frame/subscribe [:chats/chat-type chat-id])
(re-frame/subscribe [:chats/joined chat-id])]) (re-frame/subscribe [:chats/joined chat-id])])
(fn [[message-list messages loading-messages? synced-from chat-type joined] [_ chat-id]] (fn [[message-list messages pin-messages loading-messages? synced-from chat-type joined] [_ chat-id]]
;;TODO (perf) ;;TODO (perf)
(let [message-list-seq (models.message-list/->seq message-list)] (let [message-list-seq (models.message-list/->seq message-list)]
; Don't show gaps if that's the case as we are still loading messages ; Don't show gaps if that's the case as we are still loading messages
@ -984,9 +1010,26 @@
[] []
(-> message-list-seq (-> message-list-seq
(chat.db/add-datemarks) (chat.db/add-datemarks)
(hydrate-messages messages) (hydrate-messages messages pin-messages)
(chat.db/collapse-gaps chat-id synced-from (datetime/timestamp) chat-type joined loading-messages?)))))) (chat.db/collapse-gaps chat-id synced-from (datetime/timestamp) chat-type joined loading-messages?))))))
(re-frame/reg-sub
:chats/raw-chat-pin-messages-stream
(fn [[_ chat-id] _]
[(re-frame/subscribe [:chats/pin-message-list chat-id])
(re-frame/subscribe [:chats/pinned chat-id])
(re-frame/subscribe [:chats/loading-pin-messages? chat-id])
(re-frame/subscribe [:chats/synced-from chat-id])])
(fn [[pin-message-list messages loading-messages?] [_]]
;;TODO (perf)
(let [pin-message-list-seq (models.message-list/->seq pin-message-list)]
; Don't show gaps if that's the case as we are still loading messages
(if (and (empty? pin-message-list-seq) loading-messages?)
[]
(-> pin-message-list-seq
(chat.db/add-datemarks)
(hydrate-messages messages))))))
;;we want to keep data unchanged so react doesn't change component when we leave screen ;;we want to keep data unchanged so react doesn't change component when we leave screen
(def memo-chat-messages-stream (atom nil)) (def memo-chat-messages-stream (atom nil))

View File

@ -1,6 +1,7 @@
(ns ^{:doc "Definition of the StatusMessage protocol"} (ns ^{:doc "Definition of the StatusMessage protocol"}
status-im.transport.message.core status-im.transport.message.core
(:require [status-im.chat.models.message :as models.message] (:require [status-im.chat.models.message :as models.message]
[status-im.chat.models.pin-message :as models.pin-message]
[status-im.chat.models :as models.chat] [status-im.chat.models :as models.chat]
[status-im.chat.models.reactions :as models.reactions] [status-im.chat.models.reactions :as models.reactions]
[status-im.contact.core :as models.contact] [status-im.contact.core :as models.contact]
@ -11,6 +12,7 @@
[status-im.data-store.chats :as data-store.chats] [status-im.data-store.chats :as data-store.chats]
[status-im.data-store.invitations :as data-store.invitations] [status-im.data-store.invitations :as data-store.invitations]
[status-im.data-store.activities :as data-store.activities] [status-im.data-store.activities :as data-store.activities]
[status-im.data-store.messages :as data-store.messages]
[status-im.group-chats.core :as models.group] [status-im.group-chats.core :as models.group]
[status-im.utils.fx :as fx] [status-im.utils.fx :as fx]
[status-im.utils.types :as types] [status-im.utils.types :as types]
@ -38,6 +40,7 @@
^js invitations (.-invitations response-js) ^js invitations (.-invitations response-js)
^js removed-chats (.-removedChats response-js) ^js removed-chats (.-removedChats response-js)
^js activity-notifications (.-activityCenterNotifications response-js) ^js activity-notifications (.-activityCenterNotifications response-js)
^js pin-messages (.-pinMessages response-js)
sync-handler (when-not process-async process-response)] sync-handler (when-not process-async process-response)]
(cond (cond
@ -87,6 +90,13 @@
(process-next response-js sync-handler) (process-next response-js sync-handler)
(models.communities/handle-communities (types/js->clj communities-clj)))) (models.communities/handle-communities (types/js->clj communities-clj))))
(seq pin-messages)
(let [pin-messages (types/js->clj pin-messages)]
(js-delete response-js "pinMessages")
(fx/merge cofx
(process-next response-js sync-handler)
(models.pin-message/receive-signal (map data-store.messages/<-rpc pin-messages))))
(seq removed-chats) (seq removed-chats)
(let [removed-chats-clj (types/js->clj removed-chats)] (let [removed-chats-clj (types/js->clj removed-chats)]
(js-delete response-js "removedChats") (js-delete response-js "removedChats")

View File

@ -17,7 +17,8 @@
:blue "#6177E5" :blue "#6177E5"
:gray "#838C91" :gray "#838C91"
:blue-light "#23252F" :blue-light "#23252F"
:red "#FC5F5F"}) :red "#FC5F5F"
:pin-background "#34232B"})
(def light {:white "#ffffff" (def light {:white "#ffffff"
:black "#000000" :black "#000000"
@ -27,7 +28,8 @@
:mentioned-background "#def6fc" :mentioned-background "#def6fc"
:mentioned-border "#b8ecf9" :mentioned-border "#b8ecf9"
:blue-light "#ECEFFC" :blue-light "#ECEFFC"
:red "#ff2d55"}) :red "#ff2d55"
:pin-background "#FFEECC"})
(def themes {:dark dark :light light}) (def themes {:dark dark :light light})
@ -79,6 +81,9 @@
(def green "#44d058") ;; icon for successful inboud transaction (def green "#44d058") ;; icon for successful inboud transaction
(def green-transparent-10 (alpha green 0.1)) ;; icon for successful inboud transaction (def green-transparent-10 (alpha green 0.1)) ;; icon for successful inboud transaction
;; YELLOW
(def pin-background (:pin-background light)) ;; Light yellow, used as background for pinned messages
(def purple "#887af9") (def purple "#887af9")
(def orange "#FE8F59") (def orange "#FE8F59")
@ -136,5 +141,6 @@
(set! gray-transparent-40 (alpha gray 0.4)) (set! gray-transparent-40 (alpha gray 0.4))
(set! green-transparent-10 (alpha green 0.1)) (set! green-transparent-10 (alpha green 0.1))
(set! red-transparent-10 (alpha red 0.1)) (set! red-transparent-10 (alpha red 0.1))
(set! blue-transparent-10 (alpha blue 0.1))) (set! blue-transparent-10 (alpha blue 0.1))
(set! pin-background (:pin-background colors)))
(reset! theme type))) (reset! theme type)))

View File

@ -22,7 +22,8 @@
[reagent.core :as reagent] [reagent.core :as reagent]
[status-im.ui.screens.chat.components.reply :as components.reply] [status-im.ui.screens.chat.components.reply :as components.reply]
[status-im.ui.screens.chat.message.link-preview :as link-preview] [status-im.ui.screens.chat.message.link-preview :as link-preview]
[status-im.ui.screens.communities.icon :as communities.icon]) [status-im.ui.screens.communities.icon :as communities.icon]
[status-im.chat.models.pin-message :as models.pin-message])
(:require-macros [status-im.utils.views :refer [defview letsubs]])) (:require-macros [status-im.utils.views :refer [defview letsubs]]))
(defview mention-element [from] (defview mention-element [from]
@ -34,41 +35,42 @@
(defn message-timestamp (defn message-timestamp
([message] ([message]
[message-timestamp message false]) [message-timestamp message false])
([{:keys [timestamp-str outgoing content outgoing-status edited-at]} justify-timestamp?] ([{:keys [timestamp-str outgoing content outgoing-status pinned edited-at in-popover?]} justify-timestamp?]
[react/view (when justify-timestamp? (when-not in-popover? ;; We keep track if showing this message in a list in pin-limit-popover
{:align-self :flex-end [react/view (when justify-timestamp?
:position :absolute {:align-self :flex-end
:bottom 9 ; 6 Bubble bottom, 3 message baseline :position :absolute
(if (:rtl? content) :left :right) 0 :bottom 9 ; 6 Bubble bottom, 3 message baseline
:flex-direction :row (if (:rtl? content) :left :right) 0
:align-items :flex-end}) :flex-direction :row
(when (and outgoing justify-timestamp?) :align-items :flex-end})
[icons/icon (case outgoing-status (when (and outgoing justify-timestamp?)
:sending :tiny-icons/tiny-pending [icons/icon (case outgoing-status
:sent :tiny-icons/tiny-sent :sending :tiny-icons/tiny-pending
:not-sent :tiny-icons/tiny-warning :sent :tiny-icons/tiny-sent
:delivered :tiny-icons/tiny-delivered :not-sent :tiny-icons/tiny-warning
:tiny-icons/tiny-pending) :delivered :tiny-icons/tiny-delivered
{:width 16 :tiny-icons/tiny-pending)
:height 12 {:width 16
:color colors/white :height 12
:accessibility-label (name outgoing-status)}]) :color (if pinned colors/gray colors/white)
[react/text {:style (style/message-timestamp-text outgoing)} :accessibility-label (name outgoing-status)}])
(str [react/text {:style (style/message-timestamp-text (and outgoing (not pinned)))}
timestamp-str (str
(when edited-at edited-at-text))]])) timestamp-str
(when edited-at edited-at-text))]])))
(defview quoted-message (defview quoted-message
[_ {:keys [from parsed-text image]} outgoing current-public-key public?] [_ {:keys [from parsed-text image]} outgoing current-public-key public? pinned]
(letsubs [contact-name [:contacts/contact-name-by-identity from]] (letsubs [contact-name [:contacts/contact-name-by-identity from]]
[react/view {:style (style/quoted-message-container outgoing)} [react/view {:style (style/quoted-message-container (and outgoing (not pinned)))}
[react/view {:style style/quoted-message-author-container} [react/view {:style style/quoted-message-author-container}
[chat.utils/format-reply-author [chat.utils/format-reply-author
from from
contact-name contact-name
current-public-key current-public-key
(partial style/quoted-message-author outgoing) (partial style/quoted-message-author (and outgoing (not pinned)))
outgoing]] (and outgoing (not pinned))]]
(if (and image (if (and image
;; Disabling images for public-chats ;; Disabling images for public-chats
(not public?)) (not public?))
@ -77,11 +79,11 @@
:background-color :black :background-color :black
:border-radius 4} :border-radius 4}
:source {:uri image}}] :source {:uri image}}]
[react/text {:style (style/quoted-message-text outgoing) [react/text {:style (style/quoted-message-text (and outgoing (not pinned)))
:number-of-lines 5} :number-of-lines 5}
(components.reply/get-quoted-text-with-mentions parsed-text)])])) (components.reply/get-quoted-text-with-mentions parsed-text)])]))
(defn render-inline [message-text outgoing content-type acc {:keys [type literal destination]}] (defn render-inline [message-text outgoing pinned content-type acc {:keys [type literal destination]}]
(case type (case type
"" ""
(conj acc literal) (conj acc literal)
@ -93,22 +95,22 @@
literal]) literal])
"emph" "emph"
(conj acc [react/text-class (style/emph-style outgoing) literal]) (conj acc [react/text-class (style/emph-style (and outgoing (not pinned))) literal])
"strong" "strong"
(conj acc [react/text-class (style/strong-style outgoing) literal]) (conj acc [react/text-class (style/strong-style (and outgoing (not pinned))) literal])
"strong-emph" "strong-emph"
(conj acc [quo/text (style/strong-emph-style outgoing) literal]) (conj acc [quo/text (style/strong-emph-style (and outgoing (not pinned))) literal])
"del" "del"
(conj acc [react/text-class (style/strikethrough-style outgoing) literal]) (conj acc [react/text-class (style/strikethrough-style (and outgoing (not pinned))) literal])
"link" "link"
(conj acc (conj acc
[react/text-class [react/text-class
{:style {:style
{:color (if outgoing colors/white-persist colors/blue) {:color (if (and outgoing (not pinned)) colors/white-persist colors/blue)
:text-decoration-line :underline} :text-decoration-line :underline}
:on-press :on-press
#(when (and (security/safe-link? destination) #(when (and (security/safe-link? destination)
@ -121,14 +123,14 @@
(conj acc [react/text-class (conj acc [react/text-class
{:style {:color (cond {:style {:color (cond
(= content-type constants/content-type-system-text) colors/black (= content-type constants/content-type-system-text) colors/black
outgoing colors/mention-outgoing (and outgoing (not pinned)) colors/mention-outgoing
:else colors/mention-incoming)} :else colors/mention-incoming)}
:on-press (when-not (= content-type constants/content-type-system-text) :on-press (when-not (= content-type constants/content-type-system-text)
#(re-frame/dispatch [:chat.ui/show-profile literal]))} #(re-frame/dispatch [:chat.ui/show-profile literal]))}
[mention-element literal]]) [mention-element literal]])
"status-tag" "status-tag"
(conj acc [react/text-class (conj acc [react/text-class
{:style {:color (if outgoing colors/white-persist colors/blue) {:style {:color (if (and outgoing (not pinned)) colors/white-persist colors/blue)
:text-decoration-line :underline} :text-decoration-line :underline}
:on-press :on-press
#(re-frame/dispatch #(re-frame/dispatch
@ -138,19 +140,19 @@
(conj acc literal))) (conj acc literal)))
(defn render-block [{:keys [content outgoing content-type]} acc (defn render-block [{:keys [content outgoing content-type pinned in-popover?]} acc
{:keys [type ^js literal children]}] {:keys [type ^js literal children]}]
(case type (case type
"paragraph" "paragraph"
(conj acc (reduce (conj acc (reduce
(fn [acc e] (render-inline (:text content) outgoing content-type acc e)) (fn [acc e] (render-inline (:text content) outgoing pinned content-type acc e))
[react/text-class (style/text-style outgoing content-type)] [react/text-class (style/text-style (and outgoing (not pinned)) content-type in-popover?)]
children)) children))
"blockquote" "blockquote"
(conj acc [react/view (style/blockquote-style outgoing) (conj acc [react/view (style/blockquote-style (and outgoing (not pinned)))
[react/text-class (style/blockquote-text-style outgoing) [react/text-class (style/blockquote-text-style (and outgoing (not pinned)))
(.substring literal 0 (dec (.-length literal)))]]) (.substring literal 0 (dec (.-length literal)))]])
"codeblock" "codeblock"
@ -165,10 +167,10 @@
(defn render-parsed-text [message tree] (defn render-parsed-text [message tree]
(reduce (fn [acc e] (render-block message acc e)) [:<>] tree)) (reduce (fn [acc e] (render-block message acc e)) [:<>] tree))
(defn render-parsed-text-with-timestamp [{:keys [timestamp-str outgoing edited-at] :as message} tree] (defn render-parsed-text-with-timestamp [{:keys [timestamp-str outgoing edited-at in-popover?] :as message} tree]
(let [elements (render-parsed-text message tree) (let [elements (render-parsed-text message tree)
timestamp [react/text {:style (style/message-timestamp-placeholder)} timestamp [react/text {:style (style/message-timestamp-placeholder)}
(str (if outgoing " " " ") timestamp-str (when edited-at edited-at-text))] (str (if (and outgoing (not in-popover?)) " " " ") (when-not in-popover? (str timestamp-str (when edited-at edited-at-text))))]
last-element (peek elements)] last-element (peek elements)]
;; Using `nth` here as slightly faster than `first`, roughly 30% ;; Using `nth` here as slightly faster than `first`, roughly 30%
;; It's worth considering pure js structures for this code path as ;; It's worth considering pure js structures for this code path as
@ -204,6 +206,36 @@
[react/view style/not-sent-icon [react/view style/not-sent-icon
[icons/icon :main-icons/warning {:color colors/red}]]]]) [icons/icon :main-icons/warning {:color colors/red}]]]])
(defn pin-author-name [pinned-by]
(let [user-contact @(re-frame/subscribe [:multiaccount/contact])
contact-names @(re-frame/subscribe [:contacts/contact-two-names-by-identity pinned-by])]
;; We append empty spaces to the name as a workaround to make one-line and multi-line label components show correctly
(str " " (if (= pinned-by (user-contact :public-key)) (i18n/label :t/You) (first contact-names)))))
(def pin-icon-width 9)
(def pin-icon-height 15)
(defn pinned-by-indicator [outgoing display-photo? pinned-by]
[react/view {:style (style/pin-indicator outgoing display-photo?)
:accessibility-label :pinned-by}
[react/view {:style (style/pinned-by-text-icon-container)}
[react/view {:style (style/pin-icon-container)}
[icons/icon :main-icons/pin {:color colors/gray
:height pin-icon-height
:width pin-icon-width
:background-color :red}]]
[quo/text {:weight :regular
:size :small
:color :main
:style (style/pinned-by-text)}
(i18n/label :t/pinned-by)]]
[quo/text {:weight :medium
:size :small
:color :main
:style (style/pin-author-text)}
(pin-author-name pinned-by)]])
(defn message-delivery-status (defn message-delivery-status
[{:keys [chat-id message-id outgoing-status message-type]}] [{:keys [chat-id message-id outgoing-status message-type]}]
(when (and (not= constants/message-type-private-group-system-message message-type) (when (and (not= constants/message-type-private-group-system-message message-type)
@ -253,7 +285,7 @@
"Author, userpic and delivery wrapper" "Author, userpic and delivery wrapper"
[{:keys [first-in-group? display-photo? display-username? [{:keys [first-in-group? display-photo? display-username?
identicon identicon
from outgoing] from outgoing in-popover?]
:as message} content {:keys [modal close-modal]}] :as message} content {:keys [modal close-modal]}]
[react/view {:style (style/message-wrapper message) [react/view {:style (style/message-wrapper message)
:pointer-events :box-none :pointer-events :box-none
@ -266,9 +298,10 @@
[react/touchable-highlight {:on-press #(do (when modal (close-modal)) [react/touchable-highlight {:on-press #(do (when modal (close-modal))
(re-frame/dispatch [:chat.ui/show-profile from]))} (re-frame/dispatch [:chat.ui/show-profile from]))}
[photos/member-photo from identicon]])]) [photos/member-photo from identicon]])])
[react/view {:style (style/message-author-wrapper outgoing display-photo?)} [react/view {:style (style/message-author-wrapper outgoing display-photo? in-popover?)}
(when display-username? (when display-username?
[react/touchable-opacity {:style style/message-author-touchable [react/touchable-opacity {:style style/message-author-touchable
:disabled in-popover?
:on-press #(do (when modal (close-modal)) :on-press #(do (when modal (close-modal))
(re-frame/dispatch [:chat.ui/show-profile from]))} (re-frame/dispatch [:chat.ui/show-profile from]))}
[message-author-name from {:modal modal}]]) [message-author-name from {:modal modal}]])
@ -290,7 +323,7 @@
(when (not= (/ width k) (first @dimensions)) (when (not= (/ width k) (first @dimensions))
(reset! dimensions [(/ width k) image-max-height])))))) (reset! dimensions [(/ width k) image-max-height]))))))
(defn message-content-image [{:keys [content outgoing] :as message} {:keys [on-long-press]}] (defn message-content-image [{:keys [content outgoing in-popover?] :as message} {:keys [on-long-press]}]
(let [dimensions (reagent/atom [image-max-width image-max-height]) (let [dimensions (reagent/atom [image-max-width image-max-height])
visible (reagent/atom false) visible (reagent/atom false)
uri (:image content)] uri (:image content)]
@ -307,7 +340,8 @@
[react/touchable-highlight {:on-press (fn [] [react/touchable-highlight {:on-press (fn []
(reset! visible true) (reset! visible true)
(react/dismiss-keyboard!)) (react/dismiss-keyboard!))
:on-long-press on-long-press} :on-long-press on-long-press
:disabled in-popover?}
[react/view {:style (style/image-message style-opts) [react/view {:style (style/image-message style-opts)
:accessibility-label :message-image} :accessibility-label :message-image}
[react/image {:style (dissoc style-opts :outgoing) [react/image {:style (dissoc style-opts :outgoing)
@ -335,24 +369,36 @@
(def message-height-px 200) (def message-height-px 200)
(def max-message-height-px 150) (def max-message-height-px 150)
(defn on-long-press-fn [on-long-press message content] (defn pin-message [{:keys [chat-id pinned] :as message}]
(let [pinned-messages @(re-frame/subscribe [:chats/pinned chat-id])]
(if (and (not pinned) (> (count pinned-messages) 2))
(do
(js/setTimeout (fn [] (re-frame/dispatch [:dismiss-keyboard])) 500)
(re-frame/dispatch [:show-popover {:view :pin-limit
:message message
:prevent-closing? true}]))
(re-frame/dispatch [::models.pin-message/send-pin-message (assoc message :pinned (not pinned))]))))
(defn on-long-press-fn [on-long-press {:keys [pinned message-pin-enabled outgoing edit-enabled show-input?] :as message} content]
(on-long-press (on-long-press
(concat (concat
(when (:outgoing message) (when (and outgoing edit-enabled)
[{:on-press #(re-frame/dispatch [:chat.ui/edit-message message]) [{:on-press #(re-frame/dispatch [:chat.ui/edit-message message])
:label (i18n/label :t/edit)}]) :label (i18n/label :t/edit)}])
(when (:show-input? message) (when show-input?
[{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message]) [{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message])
:label (i18n/label :t/message-reply)}]) :label (i18n/label :t/message-reply)}])
[{:on-press #(react/copy-to-clipboard [{:on-press #(react/copy-to-clipboard
(components.reply/get-quoted-text-with-mentions (components.reply/get-quoted-text-with-mentions
(get content :parsed-text))) (get content :parsed-text)))
:label (i18n/label :t/sharing-copy-to-clipboard)}]))) :label (i18n/label :t/sharing-copy-to-clipboard)}]
(when message-pin-enabled [{:on-press #(pin-message message)
:label (if pinned (i18n/label :t/unpin) (i18n/label :t/pin))}]))))
(defn collapsible-text-message [{:keys [mentioned]} _] (defn collapsible-text-message [{:keys [mentioned]} _]
(let [collapsed? (reagent/atom false) (let [collapsed? (reagent/atom false)
collapsible? (reagent/atom false)] collapsible? (reagent/atom false)]
(fn [{:keys [content outgoing current-public-key public?] :as message} on-long-press modal] (fn [{:keys [content outgoing current-public-key public? pinned in-popover?] :as message} on-long-press modal]
(let [max-height (when-not (or outgoing modal) (let [max-height (when-not (or outgoing modal)
(if @collapsible? (if @collapsible?
(if @collapsed? message-height-px nil) (if @collapsed? message-height-px nil)
@ -365,7 +411,8 @@
(if @collapsed? (if @collapsed?
(do (reset! collapsed? false) (do (reset! collapsed? false)
(js/setTimeout #(on-long-press-fn on-long-press message content) 200)) (js/setTimeout #(on-long-press-fn on-long-press message content) 200))
(on-long-press-fn on-long-press message content)))}) (on-long-press-fn on-long-press message content)))
:disabled in-popover?})
[react/view {:style (style/message-view message)} [react/view {:style (style/message-view message)}
[react/view {:style (style/message-view-content) [react/view {:style (style/message-view-content)
:max-height max-height} :max-height max-height}
@ -378,13 +425,13 @@
(reset! collapsed? true) (reset! collapsed? true)
(reset! collapsible? true))} (reset! collapsible? true))}
(when (and (seq response-to) (:quoted-message message)) (when (and (seq response-to) (:quoted-message message))
[quoted-message response-to (:quoted-message message) outgoing current-public-key public?]) [quoted-message response-to (:quoted-message message) outgoing current-public-key public? pinned])
[render-parsed-text-with-timestamp message (:parsed-text content)]]) [render-parsed-text-with-timestamp message (:parsed-text content)]])
(when-not @collapsed? (when-not @collapsed?
[message-timestamp message true]) [message-timestamp message true])
(when (and @collapsible? (not modal)) (when (and @collapsible? (not modal))
(if @collapsed? (if @collapsed?
(let [color (if mentioned colors/mentioned-background colors/blue-light)] (let [color (if pinned colors/pin-background (if mentioned colors/mentioned-background colors/blue-light))]
[react/touchable-highlight [react/touchable-highlight
{:on-press #(swap! collapsed? not) {:on-press #(swap! collapsed? not)
:style {:position :absolute :bottom 0 :left 0 :right 0 :height 72}} :style {:position :absolute :bottom 0 :left 0 :right 0 :height 72}}
@ -412,48 +459,53 @@
[community-content message]) [community-content message])
(defmethod ->message constants/content-type-status (defmethod ->message constants/content-type-status
[{:keys [content content-type] :as message}] [{:keys [content content-type pinned] :as message}]
[message-content-wrapper message [message-content-wrapper message
[react/view style/status-container [react/view style/status-container
[react/text {:style (style/status-text)} [react/text {:style (style/status-text)}
(reduce (reduce
(fn [acc e] (render-inline (:text content) false content-type acc e)) (fn [acc e] (render-inline (:text content) false pinned content-type acc e))
[react/text-class {:style (style/status-text)}] [react/text-class {:style (style/status-text)}]
(-> content :parsed-text peek :children))]]]) (-> content :parsed-text peek :children))]]])
(defmethod ->message constants/content-type-emoji (defmethod ->message constants/content-type-emoji
[{:keys [content current-public-key outgoing public?] :as message} {:keys [on-long-press modal] [{:keys [content current-public-key outgoing public? pinned in-popover? message-pin-enabled] :as message} {:keys [on-long-press modal]
:as reaction-picker}] :as reaction-picker}]
(let [response-to (:response-to content)] (let [response-to (:response-to content)]
[message-content-wrapper message [message-content-wrapper message
[react/touchable-highlight (when-not modal [react/touchable-highlight (when-not modal
{:on-press (fn [] {:disabled in-popover?
:on-press (fn []
(react/dismiss-keyboard!)) (react/dismiss-keyboard!))
:on-long-press (fn [] :on-long-press (fn []
(on-long-press (on-long-press
[{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message]) (concat
:label (i18n/label :t/message-reply)} [{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message])
{:on-press #(react/copy-to-clipboard (get content :text)) :label (i18n/label :t/message-reply)}
:label (i18n/label :t/sharing-copy-to-clipboard)}]))}) {:on-press #(react/copy-to-clipboard (get content :text))
:label (i18n/label :t/sharing-copy-to-clipboard)}]
(when message-pin-enabled [{:on-press #(pin-message message)
:label (if pinned (i18n/label :t/unpin) (i18n/label :t/pin))}]))))})
[react/view (style/message-view message) [react/view (style/message-view message)
[react/view {:style (style/message-view-content)} [react/view {:style (style/message-view-content)}
[react/view {:style (style/style-message-text outgoing)} [react/view {:style (style/style-message-text outgoing)}
(when (and (seq response-to) (:quoted-message message)) (when (and (seq response-to) (:quoted-message message))
[quoted-message response-to (:quoted-message message) outgoing current-public-key public?]) [quoted-message response-to (:quoted-message message) outgoing current-public-key public? pinned])
[react/text {:style (style/emoji-message message)} [react/text {:style (style/emoji-message message)}
(:text content)]] (:text content)]]
[message-timestamp message]]]] [message-timestamp message]]]]
reaction-picker])) reaction-picker]))
(defmethod ->message constants/content-type-sticker (defmethod ->message constants/content-type-sticker
[{:keys [content from outgoing] [{:keys [content from outgoing in-popover?]
:as message} :as message}
{:keys [on-long-press modal] {:keys [on-long-press modal]
:as reaction-picker}] :as reaction-picker}]
(let [pack (get-in content [:sticker :pack])] (let [pack (get-in content [:sticker :pack])]
[message-content-wrapper message [message-content-wrapper message
[react/touchable-highlight (when-not modal [react/touchable-highlight (when-not modal
{:accessibility-label :sticker-message {:disabled in-popover?
:accessibility-label :sticker-message
:on-press (fn [_] :on-press (fn [_]
(when pack (when pack
(re-frame/dispatch [:stickers/open-sticker-pack pack])) (re-frame/dispatch [:stickers/open-sticker-pack pack]))
@ -469,10 +521,11 @@
:source {:uri (contenthash/url (-> content :sticker :hash))}}]] :source {:uri (contenthash/url (-> content :sticker :hash))}}]]
reaction-picker])) reaction-picker]))
(defmethod ->message constants/content-type-image [{:keys [content] :as message} {:keys [on-long-press modal] (defmethod ->message constants/content-type-image [{:keys [content in-popover?] :as message} {:keys [on-long-press modal]
:as reaction-picker}] :as reaction-picker}]
[message-content-wrapper message [message-content-wrapper message
[message-content-image message {:modal modal [message-content-image message {:modal modal
:disabled in-popover?
:on-long-press (fn [] :on-long-press (fn []
(on-long-press (on-long-press
[{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message]) [{:on-press #(re-frame/dispatch [:chat.ui/reply-to-message message])
@ -496,21 +549,25 @@
[message-content-wrapper message [message-content-wrapper message
[unknown-content-type message]]) [unknown-content-type message]])
(defn chat-message [message space-keeper] (defn chat-message [{:keys [outgoing display-photo? pinned pinned-by] :as message} space-keeper]
[reactions/with-reaction-picker [:<>
{:message message [reactions/with-reaction-picker
:reactions @(re-frame/subscribe [:chats/message-reactions (:message-id message) (:chat-id message)]) {:message message
:picker-on-open (fn [] :reactions @(re-frame/subscribe [:chats/message-reactions (:message-id message) (:chat-id message)])
(space-keeper true)) :picker-on-open (fn []
:picker-on-close (fn [] (space-keeper true))
(space-keeper false)) :picker-on-close (fn []
:send-emoji (fn [{:keys [emoji-id]}] (space-keeper false))
(re-frame/dispatch [::models.reactions/send-emoji-reaction :send-emoji (fn [{:keys [emoji-id]}]
{:message-id (:message-id message) (re-frame/dispatch [::models.reactions/send-emoji-reaction
:emoji-id emoji-id}])) {:message-id (:message-id message)
:retract-emoji (fn [{:keys [emoji-id emoji-reaction-id]}] :emoji-id emoji-id}]))
(re-frame/dispatch [::models.reactions/send-emoji-reaction-retraction :retract-emoji (fn [{:keys [emoji-id emoji-reaction-id]}]
{:message-id (:message-id message) (re-frame/dispatch [::models.reactions/send-emoji-reaction-retraction
:emoji-id emoji-id {:message-id (:message-id message)
:emoji-reaction-id emoji-reaction-id}])) :emoji-id emoji-id
:render ->message}]) :emoji-reaction-id emoji-reaction-id}]))
:render ->message}]
(when pinned
[react/view {:style (style/pin-indicator-container outgoing)}
[pinned-by-indicator outgoing display-photo? pinned-by]])])

View File

@ -0,0 +1,89 @@
(ns status-im.ui.screens.chat.message.pinned-message
(:require [re-frame.core :as re-frame]
[status-im.i18n.i18n :as i18n]
[status-im.ui.components.colors :as colors]
[status-im.ui.components.react :as react]
[quo.core :as quo]
[reagent.core :as reagent]
[status-im.chat.models.pin-message :as models.pin-message]
[status-im.ui.components.list.views :as list]
[status-im.ui.components.radio :as radio]
[status-im.utils.handlers :refer [<sub]]
[status-im.ui.screens.chat.message.message :as message]))
(def selected-unpin (reagent/atom nil))
(defn render-pin-fn [{:keys [message-id outgoing] :as message}
_
_
{:keys [group-chat public? current-public-key space-keeper]}]
[react/touchable-without-feedback {:style {:width "100%"}
:on-press #(reset! selected-unpin message-id)}
[react/view {:style {:flex-direction :row
:align-items :center
:justify-content :space-between
:flex 1
:padding-right 20}}
[message/chat-message
(assoc message
:group-chat group-chat
:public? public?
:current-public-key current-public-key
:show-input? false
:pinned false
:display-username? (not outgoing)
:display-photo? false
:last-in-group? false
:in-popover? true)
space-keeper]
[react/view {:style {:position :absolute
:right 18
:padding-top 4}}
[radio/radio (= @selected-unpin message-id)]]]])
(def list-key-fn #(or (:message-id %) (:value %)))
(defn pinned-messages-limit-list [chat-id]
(let [pinned-messages @(re-frame/subscribe [:chats/pinned chat-id])]
[list/flat-list
{:key-fn list-key-fn
:data (reverse (vals pinned-messages))
:render-data {:chat-id chat-id}
:render-fn render-pin-fn
:on-scroll-to-index-failed identity
:style {:flex-grow 0
:border-top-width 1
:border-bottom-width 1
:border-top-color colors/gray-lighter
:border-bottom-color colors/gray-lighter}
:content-container-style {:padding-bottom 10
:padding-top 10}}]))
(defn pin-limit-popover []
(let [{:keys [message]} (<sub [:popover/popover])]
[react/view
[react/view {:style {:height 60
:justify-content :center}}
[react/text {:style {:padding-horizontal 40
:text-align :center}}
(i18n/label :t/pin-limit-reached)]]
[pinned-messages-limit-list (message :chat-id)]
[react/view {:flex-direction :row :padding-horizontal 16 :height 60 :justify-content :space-between :align-items :center}
[quo/button
{:on-press #(do
(reset! selected-unpin nil)
(re-frame/dispatch [:hide-popover]))
:type :secondary}
(i18n/label :t/cancel)]
[quo/button
{:on-press #(do
(re-frame/dispatch [::models.pin-message/send-pin-message {:chat-id (message :chat-id)
:message-id @selected-unpin
:pinned false}])
(re-frame/dispatch [::models.pin-message/send-pin-message (assoc message :pinned true)])
(re-frame/dispatch [:hide-popover])
(reset! selected-unpin nil))
:type :secondary
:disabled (nil? @selected-unpin)
:theme (if (nil? @selected-unpin) :disabled :negative)}
(i18n/label :t/unpin)]]]))

View File

@ -0,0 +1,95 @@
(ns status-im.ui.screens.chat.pinned-messages
(:require [re-frame.core :as re-frame]
[reagent.core :as reagent]
[status-im.i18n.i18n :as i18n]
[status-im.ui.components.connectivity.view :as connectivity]
[status-im.ui.components.react :as react]
[quo.animated :as animated]
[status-im.ui.screens.chat.styles.main :as style]
[status-im.ui.screens.chat.components.accessory :as accessory]
[status-im.utils.platform :as platform]
[quo.react :as quo.react]
[status-im.ui.components.topbar :as topbar]
[status-im.ui.screens.chat.views :as chat]
[status-im.ui.components.list.views :as list]))
(defn pins-topbar []
(let [{:keys [group-chat chat-id chat-name]}
@(re-frame/subscribe [:chats/current-chat])
pinned-messages @(re-frame/subscribe [:chats/pinned chat-id])
[first-name _] (when-not group-chat @(re-frame.core/subscribe [:contacts/contact-two-names-by-identity chat-id]))]
[topbar/topbar {:show-border? true
:title (if group-chat chat-name first-name)
:subtitle (if (= (count pinned-messages) 0)
(i18n/label :t/no-pinned-messages)
(i18n/label-pluralize (count pinned-messages) :t/pinned-messages-count))}]))
(defn get-space-keeper-ios [bottom-space panel-space active-panel text-input-ref]
(fn [state]
;; NOTE: Only iOS now because we use soft input resize screen on android
(when platform/ios?
(cond
(and state
(< @bottom-space @panel-space)
(not @active-panel))
(reset! bottom-space @panel-space)
(and (not state)
(< @panel-space @bottom-space))
(do
(some-> ^js (quo.react/current-ref text-input-ref) .focus)
(reset! panel-space @bottom-space)
(reset! bottom-space 0))))))
(defn pinned-messages-empty []
[react/view {:style {:flex 1
:align-items :center
:justify-content :center}}
[react/text {:style style/intro-header-description}
(i18n/label :t/pinned-messages-empty)]])
(defonce messages-list-ref (atom nil))
(def list-ref #(reset! messages-list-ref %))
(defn pinned-messages-view [{:keys [chat pan-responder space-keeper]}]
(let [{:keys [group-chat chat-id public? community-id admins]} chat
pinned-messages @(re-frame/subscribe [:chats/raw-chat-pin-messages-stream chat-id])]
(if (= (count pinned-messages) 0)
[pinned-messages-empty]
;;do not use anonymous functions for handlers
[list/flat-list
(merge
pan-responder
{:key-fn chat/list-key-fn
:ref list-ref
:data (reverse pinned-messages)
:render-data (chat/get-render-data {:group-chat group-chat
:chat-id chat-id
:public? public?
:community-id community-id
:admins admins
:space-keeper space-keeper
:show-input? false
:edit-enabled false
:in-pinned-view? true})
:render-fn chat/render-fn
:content-container-style {:padding-top 16
:padding-bottom 16}})])))
(defn pinned-messages []
(let [bottom-space (reagent/atom 0)
panel-space (reagent/atom 52)
active-panel (reagent/atom nil)
position-y (animated/value 0)
pan-state (animated/value 0)
text-input-ref (quo.react/create-ref)
pan-responder (accessory/create-pan-responder position-y pan-state)
space-keeper (get-space-keeper-ios bottom-space panel-space active-panel text-input-ref)
chat @(re-frame/subscribe [:chats/current-chat-chat-view])]
[:<>
[pins-topbar]
[connectivity/loading-indicator]
[pinned-messages-view {:chat chat
:pan-responder pan-responder
:space-keeper space-keeper}]]))

View File

@ -8,7 +8,8 @@
[status-im.ui.components.chat-icon.screen :as chat-icon] [status-im.ui.components.chat-icon.screen :as chat-icon]
[status-im.multiaccounts.core :as multiaccounts] [status-im.multiaccounts.core :as multiaccounts]
[status-im.ui.screens.chat.styles.message.sheets :as sheets.styles] [status-im.ui.screens.chat.styles.message.sheets :as sheets.styles]
[quo.core :as quo])) [quo.core :as quo]
[status-im.chat.models.pin-message :as models.pin-message]))
(defn hide-sheet-and-dispatch [event] (defn hide-sheet-and-dispatch [event]
(re-frame/dispatch [:bottom-sheet/hide]) (re-frame/dispatch [:bottom-sheet/hide])
@ -25,7 +26,9 @@
:subtitle (i18n/label :t/view-profile) :subtitle (i18n/label :t/view-profile)
:accessibility-label :view-chat-details-button :accessibility-label :view-chat-details-button
:chevron true :chevron true
:on-press #(hide-sheet-and-dispatch [:chat.ui/show-profile chat-id])}] :on-press #(do
(hide-sheet-and-dispatch [:chat.ui/show-profile chat-id])
(re-frame/dispatch [::models.pin-message/load-pin-messages chat-id]))}]
[quo/list-item [quo/list-item
{:theme :accent {:theme :accent
:title (i18n/label :t/mark-all-read) :title (i18n/label :t/mark-all-read)
@ -91,6 +94,12 @@
:chevron true :chevron true
:accessibility-label :view-community-channel-details :accessibility-label :view-community-channel-details
:on-press #(hide-sheet-and-dispatch [:navigate-to :community-channel-details {:chat-id chat-id}])}] :on-press #(hide-sheet-and-dispatch [:navigate-to :community-channel-details {:chat-id chat-id}])}]
[quo/list-item
{:theme :accent
:title (i18n/label :t/pinned-messages)
:icon :main-icons/pin
:accessory :text
:on-press #(hide-sheet-and-dispatch [:contact.ui/pinned-messages-pressed chat-id])}]
[quo/list-item [quo/list-item
{:theme :accent {:theme :accent
:title (i18n/label :t/mark-all-read) :title (i18n/label :t/mark-all-read)
@ -123,7 +132,9 @@
:icon [chat-icon/chat-icon-view-chat-sheet :icon [chat-icon/chat-icon-view-chat-sheet
chat-id group-chat chat-name color] chat-id group-chat chat-name color]
:chevron true :chevron true
:on-press #(hide-sheet-and-dispatch [:show-group-chat-profile chat-id])}] :on-press #(do
(hide-sheet-and-dispatch [:show-group-chat-profile chat-id])
(re-frame/dispatch [::models.pin-message/load-pin-messages chat-id]))}]
[quo/list-item [quo/list-item
{:theme :accent {:theme :accent
:title (i18n/label :t/mark-all-read) :title (i18n/label :t/mark-all-read)

View File

@ -6,6 +6,11 @@
:align-items :center :align-items :center
:flex-direction :row}) :flex-direction :row})
(def pins-name-view
{:flex 1
:justify-content :center
:align-items :center})
(def chat-name-view (def chat-name-view
{:flex 1 {:flex 1
:justify-content :center}) :justify-content :center})

View File

@ -42,18 +42,18 @@
colors/white-transparent-70-persist colors/white-transparent-70-persist
colors/gray)})) colors/gray)}))
(defn message-wrapper [{:keys [outgoing]}] (defn message-wrapper [{:keys [outgoing in-popover?]}]
(if outgoing (if (and outgoing (not in-popover?))
{:margin-left 96} {:margin-left 96}
{:margin-right 52})) {:margin-right 52}))
(defn message-author-wrapper (defn message-author-wrapper
[outgoing display-photo?] [outgoing display-photo? in-popover?]
(let [align (if outgoing :flex-end :flex-start)] (let [align (if (and outgoing (not in-popover?)) :flex-end :flex-start)]
(merge {:flex-direction :column (merge {:flex-direction :column
:flex-shrink 1 :flex-shrink 1
:align-items align} :align-items align}
(if outgoing (if (and outgoing (not in-popover?))
{:margin-right 8} {:margin-right 8}
(when-not display-photo? (when-not display-photo?
{:margin-left 8}))))) {:margin-left 8})))))
@ -65,6 +65,64 @@
{:align-self :flex-start {:align-self :flex-start
:padding-left 8})) :padding-left 8}))
(defn pin-indicator [outgoing display-photo?]
(merge
{:flex-direction :row
:border-top-left-radius (if outgoing 12 4)
:border-top-right-radius (if outgoing 4 12)
:border-bottom-left-radius 12
:border-bottom-right-radius 12
:padding-left 8
:padding-right 10
:padding-vertical 5
:background-color colors/gray-lighter
:justify-content :center
:max-width "80%"}
(if outgoing
{:align-self :flex-end
:align-items :flex-end}
{:align-self :flex-start
:align-items :flex-start})
(when display-photo?
{:margin-left 44})))
(defn pin-indicator-container [outgoing]
(merge
{:margin-top 2
:align-items :center
:justify-content :center}
(if outgoing
{:align-self :flex-end
:align-items :flex-end
:padding-right 8}
{:align-self :flex-start
:align-items :flex-start
:padding-left 8})))
(defn pinned-by-text-icon-container []
{:flex-direction :row
:align-items :flex-start
:top 5
:left 8
:position :absolute})
(defn pin-icon-container []
{:flex-direction :row
:margin-top 1})
(defn pin-author-text []
{:margin-left 2
:margin-right 12
:padding-right 0
:left 12
:flex-direction :row
:flex-shrink 1
:align-self :flex-start
:overflow :hidden})
(defn pinned-by-text []
{:margin-left 5})
(def message-author-touchable (def message-author-touchable
{:margin-left 12 {:margin-left 12
:flex-direction :row}) :flex-direction :row})
@ -115,7 +173,7 @@
:shadow-offset {:width 0 :height 4}}) :shadow-offset {:width 0 :height 4}})
(defn message-view (defn message-view
[{:keys [content-type outgoing group-chat last-in-group? mentioned]}] [{:keys [content-type outgoing group-chat last-in-group? mentioned pinned]}]
(merge (merge
{:border-top-left-radius 16 {:border-top-left-radius 16
:border-top-right-radius 16 :border-top-right-radius 16
@ -134,6 +192,7 @@
{:border-bottom-left-radius 4}) {:border-bottom-left-radius 4})
(cond (cond
pinned {:background-color colors/pin-background}
(= content-type constants/content-type-system-text) nil (= content-type constants/content-type-system-text) nil
outgoing {:background-color colors/blue} outgoing {:background-color colors/blue}
mentioned {:background-color colors/mentioned-background mentioned {:background-color colors/mentioned-background
@ -214,11 +273,13 @@
:text-align :center :text-align :center
:font-weight "400")) :font-weight "400"))
(defn text-style [outgoing content-type] (defn text-style [outgoing content-type in-popover?]
(cond (merge
(= content-type constants/content-type-system-text) (system-text-style) (when in-popover? {:number-of-lines 2})
outgoing (outgoing-text-style) (cond
:else (default-text-style))) (= content-type constants/content-type-system-text) (system-text-style)
outgoing (outgoing-text-style)
:else (default-text-style))))
(defn emph-text-style [] (defn emph-text-style []
(update (default-text-style) :style (update (default-text-style) :style

View File

@ -239,8 +239,8 @@
(defn render-fn [{:keys [outgoing type] :as message} (defn render-fn [{:keys [outgoing type] :as message}
idx idx
_ _
{:keys [group-chat public? current-public-key space-keeper chat-id show-input?]}] {:keys [group-chat public? current-public-key space-keeper chat-id show-input? message-pin-enabled edit-enabled in-pinned-view?]}]
[react/view {:style (when platform/android? {:scaleY -1})} [react/view {:style (when (and platform/android? (not in-pinned-view?)) {:scaleY -1})}
(if (= type :datemark) (if (= type :datemark)
[message-datemark/chat-datemark (:value message)] [message-datemark/chat-datemark (:value message)]
(if (= type :gap) (if (= type :gap)
@ -252,7 +252,9 @@
:group-chat group-chat :group-chat group-chat
:public? public? :public? public?
:current-public-key current-public-key :current-public-key current-public-key
:show-input? show-input?) :show-input? show-input?
:message-pin-enabled message-pin-enabled
:edit-enabled edit-enabled)
space-keeper]))]) space-keeper]))])
(def list-key-fn #(or (:message-id %) (:value %))) (def list-key-fn #(or (:message-id %) (:value %)))
@ -267,10 +269,29 @@
(utils/set-timeout #(re-frame/dispatch [:chat.ui/load-more-messages-for-current-chat]) (utils/set-timeout #(re-frame/dispatch [:chat.ui/load-more-messages-for-current-chat])
(if platform/low-device? 700 200)))) (if platform/low-device? 700 200))))
(defn get-render-data [{:keys [group-chat chat-id public? community-id admins space-keeper show-input? edit-enabled in-pinned-view?]}]
(let [current-public-key @(re-frame/subscribe [:multiaccount/public-key])
community @(re-frame/subscribe [:communities/community community-id])
group-admin? (get admins current-public-key)
community-admin? (when community (community :admin))
message-pin-enabled (and (not public?)
(or (not group-chat)
(and group-chat
(or group-admin?
community-admin?))))]
{:group-chat group-chat
:public? public?
:current-public-key current-public-key
:space-keeper space-keeper
:chat-id chat-id
:show-input? show-input?
:message-pin-enabled message-pin-enabled
:edit-enabled edit-enabled
:in-pinned-view? in-pinned-view?}))
(defn messages-view [{:keys [chat bottom-space pan-responder space-keeper show-input?]}] (defn messages-view [{:keys [chat bottom-space pan-responder space-keeper show-input?]}]
(let [{:keys [group-chat chat-id public?]} chat (let [{:keys [group-chat chat-id public? community-id admins]} chat
messages @(re-frame/subscribe [:chats/chat-messages-stream chat-id]) messages @(re-frame/subscribe [:chats/chat-messages-stream chat-id])]
current-public-key @(re-frame/subscribe [:multiaccount/public-key])]
;;do not use anonymous functions for handlers ;;do not use anonymous functions for handlers
[list/flat-list [list/flat-list
(merge (merge
@ -280,12 +301,15 @@
:header [list-header chat] :header [list-header chat]
:footer [list-footer chat] :footer [list-footer chat]
:data messages :data messages
:render-data {:group-chat group-chat :render-data (get-render-data {:group-chat group-chat
:public? public? :chat-id chat-id
:current-public-key current-public-key :public? public?
:space-keeper space-keeper :community-id community-id
:chat-id chat-id :admins admins
:show-input? show-input?} :space-keeper space-keeper
:show-input? show-input?
:edit-enabled true
:in-pinned-view? false})
:render-fn render-fn :render-fn render-fn
:on-viewable-items-changed on-viewable-items-changed :on-viewable-items-changed on-viewable-items-changed
:on-end-reached list-on-end-reached :on-end-reached list-on-end-reached

View File

@ -19,7 +19,8 @@
[status-im.ui.screens.biometric.views :as biometric] [status-im.ui.screens.biometric.views :as biometric]
[status-im.ui.components.colors :as colors] [status-im.ui.components.colors :as colors]
[status-im.ui.screens.keycard.views :as keycard.views] [status-im.ui.screens.keycard.views :as keycard.views]
[status-im.ui.screens.keycard.frozen-card.view :as frozen-card])) [status-im.ui.screens.keycard.frozen-card.view :as frozen-card]
[status-im.ui.screens.chat.message.pinned-message :as pinned-message]))
(defn hide-panel-anim (defn hide-panel-anim
[bottom-anim-value alpha-value window-height] [bottom-anim-value alpha-value window-height]
@ -173,6 +174,9 @@
(= :password-reset-success view) (= :password-reset-success view)
[reset-password.views/reset-success-popover] [reset-password.views/reset-success-popover]
(= :pin-limit view)
[pinned-message/pin-limit-popover]
:else :else
[view])]]]]])))}))) [view])]]]]])))})))

View File

@ -83,6 +83,16 @@
(i18n/label :t/profile-details)]] (i18n/label :t/profile-details)]]
[render-detail contact]])) [render-detail contact]]))
(defn pin-settings [public-key pin-count]
[quo/list-item
{:title (i18n/label :t/pinned-messages)
:size :small
:accessibility-label :profile-nickname-item
:accessory :text
:accessory-text pin-count
:on-press #(re-frame/dispatch [:contact.ui/pinned-messages-pressed public-key])
:chevron true}])
(defn nickname-settings [{:keys [names]}] (defn nickname-settings [{:keys [names]}]
[quo/list-item [quo/list-item
{:title (i18n/label :t/nickname) {:title (i18n/label :t/nickname)
@ -162,6 +172,7 @@
messages @(re-frame/subscribe [:chats/profile-messages-stream current-chat-id]) messages @(re-frame/subscribe [:chats/profile-messages-stream current-chat-id])
no-messages? @(re-frame/subscribe [:chats/chat-no-messages? current-chat-id]) no-messages? @(re-frame/subscribe [:chats/chat-no-messages? current-chat-id])
muted? @(re-frame/subscribe [:chats/muted public-key]) muted? @(re-frame/subscribe [:chats/muted public-key])
pinned-messages @(re-frame/subscribe [:chats/pinned public-key])
[first-name second-name] (multiaccounts/contact-two-names contact true) [first-name second-name] (multiaccounts/contact-two-names contact true)
on-share #(re-frame/dispatch [:show-popover (merge on-share #(re-frame/dispatch [:show-popover (merge
{:view :share-chat-key {:view :share-chat-key
@ -188,6 +199,7 @@
:subtitle second-name})] :subtitle second-name})]
[react/view {:height 1 :background-color colors/gray-lighter :margin-top 8}] [react/view {:height 1 :background-color colors/gray-lighter :margin-top 8}]
[nickname-settings contact] [nickname-settings contact]
[pin-settings public-key (count pinned-messages)]
[react/view {:height 1 :background-color colors/gray-lighter}] [react/view {:height 1 :background-color colors/gray-lighter}]
[react/view {:padding-top 17 :flex-direction :row :align-items :stretch :flex 1} [react/view {:padding-top 17 :flex-direction :row :align-items :stretch :flex 1}
(for [{:keys [label] :as action} (actions contact muted?) (for [{:keys [label] :as action} (actions contact muted?)

View File

@ -181,8 +181,9 @@
(defview group-chat-profile [] (defview group-chat-profile []
(letsubs [{:keys [admins chat-id joined? chat-name color contacts] :as current-chat} [:chats/current-chat] (letsubs [{:keys [admins chat-id joined? chat-name color contacts] :as current-chat} [:chats/current-chat]
members [:contacts/current-chat-contacts] members [:contacts/current-chat-contacts]
current-pk [:multiaccount/public-key]] current-pk [:multiaccount/public-key]
pinned-messages [:chats/pinned chat-id]]
(when current-chat (when current-chat
(let [admin? (get admins current-pk) (let [admin? (get admins current-pk)
allow-adding-members? (and admin? joined? allow-adding-members? (and admin? joined?
@ -215,6 +216,13 @@
(when (pos? invitations) (when (pos? invitations)
[components.common/counter {:size 22} invitations])) [components.common/counter {:size 22} invitations]))
:on-press #(re-frame/dispatch [:navigate-to :group-chat-invite])}]) :on-press #(re-frame/dispatch [:navigate-to :group-chat-invite])}])
[quo/list-item
{:title (i18n/label :t/pinned-messages)
:icon :main-icons/pin
:accessory :text
:accessory-text (count pinned-messages)
:chevron true
:on-press #(re-frame/dispatch [:contact.ui/pinned-messages-pressed chat-id])}]
(when joined? (when joined?
[quo/list-item [quo/list-item
{:theme :negative {:theme :negative

View File

@ -105,7 +105,8 @@
[status-im.ui.screens.communities.edit-channel :as edit-channel] [status-im.ui.screens.communities.edit-channel :as edit-channel]
[status-im.ui.screens.anonymous-metrics-settings.views :as anonymous-metrics-settings] [status-im.ui.screens.anonymous-metrics-settings.views :as anonymous-metrics-settings]
[status-im.ui.components.colors :as colors] [status-im.ui.components.colors :as colors]
[status-im.ui.components.icons.icons :as icons])) [status-im.ui.components.icons.icons :as icons]
[status-im.ui.screens.chat.pinned-messages :as pin-messages]))
(def components (def components
[{:name :chat-toolbar [{:name :chat-toolbar
@ -212,6 +213,12 @@
:right-handler chat/topbar-button :right-handler chat/topbar-button
:component chat/chat} :component chat/chat}
;Pinned messages
{:name :chat-pinned-messages
;TODO custom subtitle
:options {:topBar {:visible false}}
:component pin-messages/pinned-messages}
{:name :group-chat-profile {:name :group-chat-profile
:insets {:top false} :insets {:top false}
;;TODO animated-header ;;TODO animated-header

View File

@ -1595,5 +1595,16 @@
"status-is-open-source": "Status is open-source", "status-is-open-source": "Status is open-source",
"build-yourself": "To use the app without these Terms of Service, you can build your own version", "build-yourself": "To use the app without these Terms of Service, you can build your own version",
"accept-and-continue": "Accept and continue", "accept-and-continue": "Accept and continue",
"empty-activity-center": "Your chat notifications\nwill appear here" "empty-activity-center": "Your chat notifications\nwill appear here",
"pinned-messages": "Pinned messages",
"pin": "Pin",
"unpin": "Unpin",
"no-pinned-messages": "No pinned messages",
"pinned-messages-count": {
"one": "1 pinned message",
"other": "{{count}} pinned messages"
},
"pinned-messages-empty": "Pinned messages will appear here. To pin a message, press and hold it and tap `Pin`",
"pinned-by": "Pinned by",
"pin-limit-reached": "Pin limit reached. Unpin a previous message first."
} }