public group chat

This commit is contained in:
Roman Volosovskyi 2017-02-07 12:56:39 +02:00 committed by Roman Volosovskyi
parent 0951618ad8
commit ace355515a
71 changed files with 736 additions and 237 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 875 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -202,5 +202,14 @@ var TopLevel = {
"IBGLog": function() {},
"getCardId": function() {},
"readTag": function() {},
"writeTag": function() {}
"writeTag": function() {},
"Error": function() {},
"Linking": function() {},
"reload": function() {},
"stop": function() {},
"toLocaleString": function() {},
"openURL": function() {},
"Number" : function () {},
"toAscii" : function () {},
"toNumber" : function () {}
};

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "con_group_chat.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "con_group_chat-1.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "con_group_chat-2.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "icon_private_group_big.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "icon_private_group_big-1.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "icon_private_group_big-2.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "icon_public_group.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "icon_public_group-1.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "icon_public_group-2.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "icon_public_group_big.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "icon_public_group_big-1.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "icon_public_group_big-2.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -23,7 +23,8 @@
[status-im.accounts.screen :refer [accounts]]
[status-im.transactions.screen :refer [confirm]]
[status-im.chats-list.screen :refer [chats-list]]
[status-im.new-group.screen :refer [new-group]]
[status-im.new-group.screen-private :refer [new-group]]
[status-im.new-group.screen-public :refer [new-public-group]]
[status-im.participants.views.add :refer [new-participants]]
[status-im.participants.views.remove :refer [remove-participants]]
[status-im.group-settings.screen :refer [group-settings]]
@ -98,6 +99,7 @@
:remove-participants remove-participants
:chat-list main-tabs
:new-group new-group
:new-public-group new-public-group
:group-settings group-settings
:contact-list main-tabs
:contact-list-search-results contacts-search-results

View File

@ -20,7 +20,7 @@
:additional-height 0}
:chat {:new-message {:border-top-color styles/color-transparent
:border-top-width 0.5}}
:discover {:subtitle {:color styles/color-gray2
:discover {:subtitle {:color styles/color-gray2
:font-size 14}
:popular {:border-radius 1
:margin-top 2
@ -72,14 +72,17 @@
;; Structure to be exported
(def platform-specific
{:component-styles component-styles
:fonts fonts
:list-selection-fn show-dialog
:tabs {:tab-shadows? true}
:chats {:action-button? true
:new-chat-in-toolbar? false}
:contacts {:action-button? true
:new-contact-in-toolbar? false
:uppercase-subtitles? false
:group-block-shadows? true}
:discover {:uppercase-subtitles? false}})
{:component-styles component-styles
:fonts fonts
:list-selection-fn show-dialog
:tabs {:tab-shadows? true}
:chats {:action-button? true
:new-chat-in-toolbar? false}
:contacts {:action-button? true
:new-contact-in-toolbar? false
:uppercase-subtitles? false
:group-block-shadows? true}
:discover {:uppercase-subtitles? false}
:public-group-icon-container {:margin-top 4}
:private-group-icon-container {:margin-top 6}
:public-group-chat-hash-style {:top 10 :left 4}})

View File

@ -511,17 +511,18 @@
(after (fn [_ _] (dispatch [:navigation-replace :chat-list])))
(u/side-effect!
(fn [{:keys [web3 current-chat-id chats current-public-key]} _]
(let [{:keys [public-key private-key]} (chats current-chat-id)]
(let [{:keys [public-key private-key public?]} (chats current-chat-id)]
(protocol/stop-watching-group!
{:web3 web3
:group-id current-chat-id})
(protocol/leave-group-chat!
{:web3 web3
:group-id current-chat-id
:keypair {:public public-key
:private private-key}
:message {:from current-public-key
:message-id (random/id)}}))
(when-not public?
(protocol/leave-group-chat!
{:web3 web3
:group-id current-chat-id
:keypair {:public public-key
:private private-key}
:message {:from current-public-key
:message-id (random/id)}})))
(dispatch [:remove-chat current-chat-id]))))
(register-handler :remove-chat
@ -563,12 +564,13 @@
[{:keys [web3 current-public-key chats]}
[_ {:keys [from chat-id message-id]}]]
(when-not (console? chat-id)
(let [{:keys [group-chat]} (chats chat-id)]
(protocol/send-seen! {:web3 web3
:message {:from current-public-key
:to from
:group-id (when group-chat chat-id)
:message-id message-id}}))))
(let [{:keys [group-chat public?]} (chats chat-id)]
(when-not public?
(protocol/send-seen! {:web3 web3
:message {:from current-public-key
:to from
:group-id (when group-chat chat-id)
:message-id message-id}})))))
(register-handler :send-seen!
[(after (fn [_ [_ {:keys [message-id]}]]
(messages/update {:message-id message-id
@ -579,7 +581,7 @@
(defn send-clock-value-request!
[{:keys [web3 current-public-key]} [_ {:keys [message-id from]}]]
(protocol/send-clock-value-request! {:web3 web3
(protocol/send-clock-value-request! {:web3 web3
:message {:from current-public-key
:to from
:message-id message-id}}))
@ -605,9 +607,9 @@
(register-handler :send-clock-value!
(u/side-effect!
(fn [db [_ to message-id]]
(let [{:keys [clock-value]} (messages/get-by-id message-id)]
(send-clock-value! db to message-id clock-value)))))
(fn [db [_ to message-id]]
(let [{:keys [clock-value]} (messages/get-by-id message-id)]
(send-clock-value! db to message-id clock-value)))))
(register-handler :set-web-view-url
(fn [{:keys [current-chat-id] :as db} [_ url]]
@ -663,7 +665,7 @@
(register-handler :update-message-overhead!
(u/side-effect!
(fn [_ [_ chat-id network-status]]
(if (= network-status :offline)
(chats/inc-message-overhead chat-id)
(chats/reset-message-overhead chat-id)))))
(fn [_ [_ chat-id network-status]]
(if (= network-status :offline)
(chats/inc-message-overhead chat-id)
(chats/reset-message-overhead chat-id)))))

View File

@ -25,23 +25,23 @@
to-message handler-data content-type]}]
(let [content (or request {:command (command :name)
:params params})]
{:message-id id
:from identity
:to chat-id
:timestamp (time/now-ms)
:content (assoc content :handler-data handler-data
:type (name (:type command))
:content-command (:name command))
:content-type (or content-type
(if request
content-type-command-request
content-type-command))
:outgoing true
:to-message to-message
:type (:type command)
:has-handler (:has-handler command)
:clock-value (inc clock-value)
:show? true}))
{:message-id id
:from identity
:to chat-id
:timestamp (time/now-ms)
:content (assoc content :handler-data handler-data
:type (name (:type command))
:content-command (:name command))
:content-type (or content-type
(if request
content-type-command-request
content-type-command))
:outgoing true
:to-message to-message
:type (:type command)
:has-handler (:has-handler command)
:clock-value (inc clock-value)
:show? true}))
(register-handler :send-chat-message
(u/side-effect!
@ -173,7 +173,7 @@
(register-handler ::prepare-message
(u/side-effect!
(fn [{:keys [network-status] :as db} [_ {:keys [chat-id identity message] :as params}]]
(let [{:keys [group-chat]} (get-in db [:chats chat-id])
(let [{:keys [group-chat public?]} (get-in db [:chats chat-id])
clock-value (messages/get-last-clock-value chat-id)
message' (cu/check-author-direction
db chat-id
@ -186,9 +186,13 @@
:timestamp (time/now-ms)
:clock-value (inc clock-value)
:show? true})
message'' (if group-chat
(assoc message' :group-id chat-id :message-type :group-user-message)
(assoc message' :to chat-id :message-type :user-message))
message'' (cond-> message'
(and group-chat public?)
(assoc :group-id chat-id :message-type :public-group-user-message)
(and group-chat (not public?))
(assoc :group-id chat-id :message-type :group-user-message)
(not group-chat)
(assoc :to chat-id :message-type :user-message))
params' (assoc params :message message'')]
(dispatch [:update-message-overhead! chat-id network-status])
(dispatch [:set-chat-ui-props :sending-disabled? false])
@ -258,7 +262,7 @@
(register-handler ::send-message!
(u/side-effect!
(fn [{:keys [web3 chats network-status]
(fn [{:keys [web3 chats network-status current-account-id accounts]
:as db} [_ {{:keys [message-type]
:as message} :message
chat-id :chat-id}]]
@ -274,23 +278,33 @@
payload)
options {:web3 web3
:message (assoc message' :payload payload)}]
(if (= message-type :group-user-message)
(cond
(= message-type :group-user-message)
(let [{:keys [public-key private-key]} (chats chat-id)]
(protocol/send-group-message! (assoc options
:group-id chat-id
:keypair {:public public-key
:private private-key})))
(= message-type :public-group-user-message)
(protocol/send-public-group-message!
(let [username (get-in accounts [current-account-id :name])]
(assoc options :group-id chat-id
:username username)))
:else
(protocol/send-message! (assoc-in options
[:message :to] (:to message)))))))))))
(register-handler ::send-command-protocol!
(u/side-effect!
(fn [{:keys [web3 current-public-key chats network-status] :as db}
[_ {:keys [chat-id command command-message]}]]
(fn [{:keys [web3 current-public-key chats network-status
current-account-id accounts] :as db}
[_ {:keys [chat-id command]}]]
(log/debug "sending command: " command)
(when (cu/not-console? chat-id)
(let [{:keys [public-key private-key]} (chats chat-id)
{:keys [group-chat]} (get-in db [:chats chat-id])
{:keys [group-chat public?]} (get-in db [:chats chat-id])
payload (-> command
(select-keys [:content :content-type
@ -303,10 +317,19 @@
:message {:from current-public-key
:message-id (:message-id command)
:payload payload}}]
(if group-chat
(cond
(and group-chat (not public?))
(protocol/send-group-message! (assoc options
:group-id chat-id
:keypair {:public public-key
:private private-key}))
(and group-chat public?)
(protocol/send-public-group-message!
(let [username (get-in accounts [current-account-id :name])]
(assoc options :group-id chat-id
:username username)))
:else
(protocol/send-message! (assoc-in options
[:message :to] chat-id))))))))

View File

@ -4,7 +4,8 @@
color-blue
selected-message-color
text1-color
text2-color]]
text2-color
color-gray]]
[status-im.constants :refer [text-content-type
content-type-command]]))
@ -125,6 +126,9 @@
(when selected
{:backgroundColor selected-message-color}))))
(def author
{:color color-gray})
(def comand-request-view
{:paddingRight 16})

View File

@ -54,9 +54,10 @@
:fontSize 16})
(def group-icon
{:marginTop 4
:width 14
:height 9})
{:margin-top 4
:margin-bottom 2.7
:width 14
:height 9})
(def up-icon
{:width 14

View File

@ -70,11 +70,11 @@
:height 13}
:handler #(dispatch [:show-group-settings])})
(defn group-chat-items [members]
[(item-members members)
item-search
item-notifications
item-settings])
(defn group-chat-items [members public?]
(into (if public? [] [(item-members members)])
[item-search
item-notifications
item-settings]))
(defn user-chat-items [chat-id]
[(item-user chat-id)
@ -114,21 +114,22 @@
subtitle])]]])
(defn actions-list-view []
(let [{:keys [group-chat chat-id]}
(subscribe [:chat-properties [:group-chat :chat-id]])
(let [{:keys [group-chat chat-id public?]}
(subscribe [:chat-properties [:group-chat :chat-id :public?]])
members (subscribe [:current-chat-contacts])
status-bar-height (get-in platform-specific [:component-styles :status-bar :default :height])]
(when-let [actions (if @group-chat
(group-chat-items @members)
(user-chat-items @chat-id))]
[view (merge
(st/actions-wrapper status-bar-height)
(get-in platform-specific [:component-styles :actions-list-view]))
[view st/actions-separator]
[view st/actions-view
(for [action actions]
(if action
^{:key action} [action-view action]))]])))
(fn []
(when-let [actions (if @group-chat
(group-chat-items @members @public?)
(user-chat-items @chat-id))]
[view (merge
(st/actions-wrapper status-bar-height)
(get-in platform-specific [:component-styles :actions-list-view]))
[view st/actions-separator]
[view st/actions-view
(for [action actions]
(if action
^{:key action} [action-view action]))]]))))
(defn actions-view []
[overlay {:on-click-outside #(dispatch [:set-chat-ui-props :show-actions? false])}

View File

@ -46,10 +46,10 @@
members (subscribe [:current-chat-contacts])]
(fn [{:keys [messages-count content datemark]}]
(let [{:keys [status]} (if @group-chat
{:photo-path nil
:status nil
:last-online 0}
(first @members))]
{:photo-path nil
:status nil
:last-online 0}
(first @members))]
[view st/status-container
[chat-icon-message-status @chat-id @group-chat @name @color false]
[text {:style st/status-from
@ -143,11 +143,10 @@
:current-chat-id current-chat-id}]]))
(defn message-view
[message content]
[{:keys [username same-author index] :as message} content]
[view (st/message-view message)
#_(when incoming-group
[text {:style message-author-text}
"Justas"])
(when (and username (or (= 1 index) (not same-author)))
[text {:style st/author} username])
content])
(defmulti message-content (fn [_ message _]
@ -202,7 +201,7 @@
(defn text-message
[{:keys [content] :as message}]
[message-view message
(let [parsed-text (parse-text content)
(let [parsed-text (parse-text content)
simple-text? (= (count parsed-text) 2)]
(if simple-text?
[autolink {:style (st/text-message message)
@ -374,10 +373,10 @@
children)])}))
(into [view] children)))
(defn chat-message [{:keys [outgoing message-id chat-id user-statuses from content] :as message}]
(let [my-identity (subscribe [:get :current-public-key])
status (subscribe [:get-in [:message-data :user-statuses message-id my-identity]])
preview (subscribe [:get-in [:message-data :preview message-id]])]
(defn chat-message [{:keys [outgoing message-id chat-id user-statuses from] :as message}]
(let [my-identity (subscribe [:get :current-public-key])
status (subscribe [:get-in [:message-data :user-statuses message-id my-identity]])
preview (subscribe [:get-in [:message-data :preview message-id]])]
(r/create-class
{:component-will-mount
(fn []

View File

@ -49,22 +49,31 @@
:synced (label :t/sync-synced)
online-text)}])
(defn group-last-activity [{:keys [contacts sync-state]}]
(defn group-last-activity [{:keys [contacts sync-state public?]}]
(if (or (= sync-state :in-progress)
(= sync-state :synced))
[last-activity {:sync-state sync-state}]
[view {:flex-direction :row}
[icon :group st/group-icon]
[text {:style st/members
:font :medium}
(let [cnt (inc (count contacts))]
(label-pluralize cnt :t/members-active))]]))
(if public?
[view {:flex-direction :row}
[text {:font :default
:style (get-in platform-specific [:component-styles :toolbar-last-activity])}
(label :t/public-group-status)]]
[view {:flex-direction :row}
[icon :group st/group-icon]
[text {:style st/members
:font :medium}
(if public?
(label :t/public-group-status)
(let [cnt (inc (count contacts))]
(label-pluralize cnt :t/members-active)))]])))
(defn toolbar-content-view []
(let [{:keys [group-chat
name
contacts
chat-id]} (subscribe [:chat-properties [:group-chat :name :contacts :chat-id]])
chat-id
public?]}
(subscribe [:chat-properties [:group-chat :name :contacts :chat-id :public?]])
show-actions? (subscribe [:chat-ui-props :show-actions?])
accounts (subscribe [:get :accounts])
contact (subscribe [:get-in [:contacts @chat-id]])
@ -72,15 +81,19 @@
(fn []
[view (st/chat-name-view (or (empty? @accounts)
@show-actions?))
[text {:style st/chat-name-text
:number-of-lines 1
:font :toolbar-title}
(if (str/blank? @name)
(generate-gfy)
(or (get-contact-translated @chat-id :name @name)
(label :t/chat-name)))]
(let [chat-name (if (str/blank? @name)
(generate-gfy)
(or (get-contact-translated @chat-id :name @name)
(label :t/chat-name)))]
[text {:style st/chat-name-text
:number-of-lines 1
:font :toolbar-title}
(if @public?
(str "#" chat-name)
chat-name)])
(if @group-chat
[group-last-activity {:contacts @contacts
:public? @public?
:sync-state @sync-state}]
[last-activity {:online-text (online-text @contact @chat-id)
:sync-state @sync-state}])])))

View File

@ -6,6 +6,7 @@
view
animated-view
text
icon
image
touchable-highlight]]
[status-im.components.action-button :refer [action-button
@ -55,8 +56,12 @@
{:title (label :t/new-group-chat)
:buttonColor :#1abc9c
:onPress #(dispatch [:navigate-to :new-group])}
[ion-icon {:name :md-person
:style st/person-stalker-icon}]]])
[icon :private_group_big st/group-icon]]
[action-button-item
{:title (label :t/new-public-group-chat)
:buttonColor :#1abc9c
:onPress #(dispatch [:navigate-to :new-public-group])}
[icon :public_group_big st/group-icon]]])
(defn chat-shadow-item []
[view {:height 3}

View File

@ -32,9 +32,9 @@
:border-bottom-color color-separator})
(def chat-container
{:flex-direction :row
:background-color color-white
:height 94})
{:flex-direction :row
:background-color color-white
:height 94})
(def chat-icon-container
{:margin-top -2
@ -44,24 +44,41 @@
:height 48})
(def item-container
{:flex-direction :column
:margin-left 30
:padding-top 16
:padding-right 16
:flex 1})
{:flex-direction :column
:margin-left 30
:padding-top 16
:padding-right 16
:flex 1})
(def name-view
{:flex-direction :row})
(def name-text
{:color text1-color
:font-size 14})
{:color text1-color
:font-size 14})
(def group-icon
{:margin-top 5
:margin-left 8
:width 14
:height 9})
(def private-group-icon-container
{:width 16
:height 9
:padding-top -4
:margin-top (get-in p/platform-specific [:private-group-icon-container :margin-top])
:margin-right 6})
(def private-group-icon
{:width 16
:height 16})
(def public-group-icon-container
{:width 16
:height 12
:padding-top -2
:margin-top (get-in p/platform-specific [:public-group-icon-container :margin-top])
:margin-right 6})
(def public-group-icon
{:width 16
:height 16
:margin-bottom -20})
(def memebers-text
{:marginTop 2
@ -124,7 +141,7 @@
:height 22
:color color-white})
(def person-stalker-icon
{:fontSize 20
:height 22
:color color-white})
(def group-icon
{:height 22
:width 22
:tint-color :white})

View File

@ -77,24 +77,34 @@
unviewed-messages]]))
(defn chat-list-item-inner-view [{:keys [chat-id name color last-message
online group-chat contacts] :as chat}]
online group-chat contacts public?]
:as chat}]
(let [last-message (or (first (sort-by :clock-value > (:messages chat)))
last-message)
name (or (get-contact-translated chat-id :name name)
(generate-gfy))]
(generate-gfy))
private-group? (and group-chat (not public?))
public-group? (and group-chat public?)]
[view st/chat-container
[view st/chat-icon-container
[chat-icon-view-chat-list chat-id group-chat name color online]]
[view st/item-container
[view st/name-view
[text {:style st/name-text
:font :medium}
(if (str/blank? name)
(generate-gfy)
(truncate-str name 30))]
(when group-chat
[icon :group st/group-icon])
(when group-chat
(when public-group?
[view st/public-group-icon-container
[icon :public_group st/public-group-icon]])
(when private-group?
[view st/private-group-icon-container
[icon :private_group st/private-group-icon]])
(let [chat-name (if (str/blank? name)
(generate-gfy)
(truncate-str name 30))]
[text {:style st/name-text
:font :medium}
(if public-group?
(str "#" chat-name)
chat-name)])
#_(when private-group?
[text {:style st/memebers-text}
(label-pluralize (inc (count contacts)) :t/members)])]
[message-content-text last-message]]

View File

@ -34,11 +34,12 @@
:on-focus #()
:on-blur #()
:on-change-text #()
:on-change #()})
:on-change #()
:auto-capitalize :sentences})
(defn field-animation [{:keys [top to-top font-size to-font-size
line-width to-line-width]}]
(let [duration (:label-animation-duration config)
(let [duration (:label-animation-duration config)
animation (anim/parallel [(anim/timing top {:toValue to-top
:duration duration})
(anim/timing font-size {:toValue to-font-size
@ -101,10 +102,14 @@
label-font-size
line-width
current-value
max-line-width]} (r/state component)
max-line-width
valid-value
temp-value
max-length]} (r/state component)
{:keys [wrapper-style input-style label-hidden? line-color focus-line-color secure-text-entry
label-color error-color error label value on-focus on-blur
on-change-text on-change on-end-editing editable placeholder]} (merge default-props (r/props component))
label-color error-color error label value on-focus on-blur validator auto-focus
on-change-text on-change on-end-editing editable placeholder auto-capitalize]}
(merge default-props (r/props component))
line-color (if error error-color line-color)
focus-line-color (if error error-color focus-line-color)
label-color (if (and error (not float-label?)) error-color label-color)
@ -118,6 +123,7 @@
:placeholder (or placeholder "")
:editable editable
:secure-text-entry secure-text-entry
:auto-capitalize auto-capitalize
:on-focus #(on-input-focus {:component component
:animation {:top label-top
:to-top (:label-top config)
@ -137,21 +143,34 @@
:onBlur on-blur})
:on-change-text (fn [text]
(r/set-state component {:current-value text})
(on-change-text text))
(if (or (not validator) (validator text))
(do
(r/set-state component {:valid-value text
:temp-value nil})
(on-change-text text))
(r/set-state component {:temp-value valid-value
:max-length (count valid-value)})))
:on-change #(on-change %)
:default-value value
:value temp-value
:max-length max-length
:on-submit-editing #(.blur @input-ref)
:on-end-editing (when on-end-editing
on-end-editing)}]
:on-end-editing (when on-end-editing on-end-editing)
:auto-focus (true? auto-focus)}]
[view {:style (st/underline-container line-color)
:onLayout #(r/set-state component {:max-line-width (get-width %)})}
[animated-view {:style (st/underline focus-line-color line-width)}]]
[text {:style (st/error-text error-color)} error]]))
(defn text-field [_ _]
(let [component-data {:get-initial-state get-initial-state
:component-will-mount component-will-mount
:display-name "text-field"
:reagent-render reagent-render}]
(let [component-data {:get-initial-state get-initial-state
:component-will-mount component-will-mount
:display-name "text-field"
:reagent-render reagent-render
:component-did-update (fn [comp]
(let [{:keys [temp-value]} (r/state comp)]
(when temp-value
(r/set-state comp {:temp-value nil
:max-length nil}))))}]
;(log/debug "Creating text-field component: " data)
(r/create-class component-data)))

View File

@ -132,6 +132,10 @@
:top 16
:left 13})
(def group-icon
(assoc option-inner-image
:tint-color color-gray))
(def spacing-top
{:background-color color-white
:height 8})

View File

@ -21,24 +21,35 @@
[status-im.contacts.views.contact-inner :refer [contact-inner-view]]))
(defn new-group-chat-view []
[touchable-highlight
{:on-press #(dispatch [:navigate-to :new-group])}
[view st/contact-container
[view st/option-inner-container
[view st/option-inner
[image {:source {:uri :icon_menu_group}
:style st/option-inner-image}]]
[view st/info-container
[text {:style st/name-text}
(label :t/new-group-chat)]]]]])
[view
[touchable-highlight
{:on-press #(dispatch [:navigate-to :new-group])}
[view st/contact-container
[view st/option-inner-container
[view st/option-inner
[image {:source {:uri :icon_private_group_big}
:style st/group-icon}]]
[view st/info-container
[text {:style st/name-text}
(label :t/new-group-chat)]]]]]
[touchable-highlight
{:on-press #(dispatch [:navigate-to :new-public-group])}
[view st/contact-container
[view st/option-inner-container
[view st/option-inner
[image {:source {:uri :icon_public_group_big}
:style st/group-icon}]]
[view st/info-container
[text {:style st/name-text}
(label :t/new-public-group-chat)]]]]]])
(defn render-row [chat-modal click-handler action params]
(fn [row _ _]
(list-item
[contact-view {:contact row
:letter? chat-modal
:on-click (if click-handler
#(click-handler row action params))}])))
[contact-view {:contact row
:letter? chat-modal
:on-click (when click-handler
#(click-handler row action params))}])))
(defn contact-list-entry [{:keys [click-handler icon icon-style label]}]
[touchable-highlight

View File

@ -29,12 +29,19 @@
(defn get-active-group-chats
[]
(map
(fn [{:keys [chat-id public-key private-key]}]
{:chat-id chat-id
:keypair {:private private-key
:public public-key}})
(fn [{:keys [chat-id public-key private-key public?]}]
(let [group {:group-id chat-id
:public? public?}]
(if (and public-key private-key)
(assoc group :keypair {:private private-key
:public public-key})
group)))
(realm/realm-collection->list (groups true))))
(defn- get-by-id-obj
[chat-id]
(realm/get-one-by-field @realm/account-realm :chat :chat-id chat-id))
(defn get-by-id
[chat-id]
(-> @realm/account-realm
@ -56,12 +63,12 @@
(defn set-inactive
[chat-id]
(when-let [chat (get-by-id chat-id)]
(when-let [chat (get-by-id-obj chat-id)]
(realm/write @realm/account-realm
(fn []
(doto chat
(aset "is-active" false)
(aset "removed-at" timestamp))))))
(aset "removed-at" (timestamp)))))))
(defn get-contacts
[chat-id]
@ -72,8 +79,8 @@
(defn has-contact?
[chat-id identity]
(let [contacts (get-contacts chat-id)
contact (.find contacts (fn [object _ _]
(= identity (aget object "identity"))))]
contact (.find contacts (fn [object _ _]
(= identity (aget object "identity"))))]
(if contact true false)))
(defn- save-contacts
@ -97,9 +104,9 @@
(defn- delete-contacts
[identities contacts]
(doseq [contact-identity identities]
(when-let [contact (.find contacts (fn [object _ _]
(= contact-identity (aget object "identity"))))]
(realm/delete @realm/account-realm contact))))
(when-let [contact (.find contacts (fn [object _ _]
(= contact-identity (aget object "identity"))))]
(realm/delete @realm/account-realm contact))))
(defn remove-contacts
[chat-id identities]

View File

@ -1,7 +1,9 @@
(ns status-im.data-store.realm.schemas.account.core
(:require [status-im.data-store.realm.schemas.account.v1.core :as v1]
[status-im.data-store.realm.schemas.account.v2.core :as v2]
[status-im.data-store.realm.schemas.account.v3.core :as v3]))
[status-im.data-store.realm.schemas.account.v3.core :as v3]
[status-im.data-store.realm.schemas.account.v4.core :as v4]
))
; put schemas ordered by version
(def schemas [{:schema v1/schema
@ -12,4 +14,7 @@
:migration v2/migration}
{:schema v3/schema
:schemaVersion 3
:migration v3/migration}])
:migration v3/migration}
{:schema v4/schema
:schemaVersion 4
:migration v4/migration}])

View File

@ -0,0 +1,42 @@
(ns status-im.data-store.realm.schemas.account.v4.chat
(:require [taoensso.timbre :as log]
[status-im.components.styles :refer [default-chat-color]]))
(def schema {:name :chat
:primaryKey :chat-id
:properties {:chat-id :string
:name :string
:color {:type :string
:default default-chat-color}
:group-chat {:type :bool
:indexed true}
:group-admin {:type :string
:optional true}
:is-active :bool
:timestamp :int
:contacts {:type :list
:objectType :chat-contact}
:removed-at {:type :int
:optional true}
:removed-from-at {:type :int
:optional true}
:added-to-at {:type :int
:optional true}
:updated-at {:type :int
:optional true}
:last-message-id :string
:message-overhead {:type :int
:default 0}
:public-key {:type :string
:optional true}
:private-key {:type :string
:optional true}
:contact-info {:type :string
:optional true}
:debug? {:type :bool
:default false}
:public? {:type :bool
:default false}}})
(defn migration [old-realm new-realm]
(log/debug "migrating chat schema v4"))

View File

@ -0,0 +1,32 @@
(ns status-im.data-store.realm.schemas.account.v4.core
(:require [status-im.data-store.realm.schemas.account.v4.chat :as chat]
[status-im.data-store.realm.schemas.account.v1.chat-contact :as chat-contact]
[status-im.data-store.realm.schemas.account.v1.command :as command]
[status-im.data-store.realm.schemas.account.v3.contact :as contact]
[status-im.data-store.realm.schemas.account.v1.discover :as discover]
[status-im.data-store.realm.schemas.account.v1.kv-store :as kv-store]
[status-im.data-store.realm.schemas.account.v4.message :as message]
[status-im.data-store.realm.schemas.account.v1.pending-message :as pending-message]
[status-im.data-store.realm.schemas.account.v1.processed-message :as processed-message]
[status-im.data-store.realm.schemas.account.v1.request :as request]
[status-im.data-store.realm.schemas.account.v1.tag :as tag]
[status-im.data-store.realm.schemas.account.v1.user-status :as user-status]
[taoensso.timbre :as log]))
(def schema [chat/schema
chat-contact/schema
command/schema
contact/schema
discover/schema
kv-store/schema
message/schema
pending-message/schema
processed-message/schema
request/schema
tag/schema
user-status/schema])
(defn migration [old-realm new-realm]
(log/debug "migrating v4 account database: " old-realm new-realm)
(chat/migration old-realm new-realm)
(contact/migration old-realm new-realm))

View File

@ -0,0 +1,38 @@
(ns status-im.data-store.realm.schemas.account.v4.message
(:require [taoensso.timbre :as log]))
(def schema {:name :message
:primaryKey :message-id
:properties {:message-id :string
:from :string
:to {:type :string
:optional true}
:group-id {:type :string
:optional true}
:content :string ;; TODO make it ArrayBuffer
:content-type :string
:username {:type :string
:optional true}
:timestamp :int
:chat-id {:type :string
:indexed true}
:outgoing :bool
:retry-count {:type :int
:default 0}
:same-author :bool
:same-direction :bool
:preview {:type :string
:optional true}
:message-type {:type :string
:optional true}
:message-status {:type :string
:optional true}
:user-statuses {:type :list
:objectType "user-status"}
:clock-value {:type :int
:default 0}
:show? {:type :bool
:default true}}})
(defn migration [_ _]
(log/debug "migrating message schema"))

View File

@ -160,11 +160,12 @@
(label :t/add-members)]]]
[chat-members]]))
(defn group-settings []
(defview group-settings []
[public? [:chat :public?]]
[view st/group-settings
[new-group-toolbar]
[scroll-view st/body
[chat-name]
(when-not public? [chat-name])
[members]
[text {:style st/settings-text}
(label :t/settings)]

View File

@ -22,7 +22,8 @@
[status-im.accounts.screen :refer [accounts]]
[status-im.transactions.screen :refer [confirm]]
[status-im.chats-list.screen :refer [chats-list]]
[status-im.new-group.screen :refer [new-group]]
[status-im.new-group.screen-private :refer [new-group]]
[status-im.new-group.screen-public :refer [new-public-group]]
[status-im.participants.views.add :refer [new-participants]]
[status-im.participants.views.remove :refer [remove-participants]]
[status-im.group-settings.screen :refer [group-settings]]
@ -86,6 +87,7 @@
:remove-participants remove-participants
:chat-list main-tabs
:new-group new-group
:new-public-group new-public-group
:group-settings group-settings
:contact-list main-tabs
:contact-list-search-results contacts-search-results

View File

@ -23,7 +23,7 @@
:border-bottom-width 0.5}
:chat {:new-message {:border-top-color styles/color-gray3
:border-top-width 0.5}}
:discover {:subtitle {:color styles/color-steel
:discover {:subtitle {:color styles/color-steel
:font-size 13
:letter-spacing 1}
:popular {:border-radius 3
@ -79,15 +79,18 @@
;; Structure to be exported
(def platform-specific
{:component-styles component-styles
:fonts fonts
:list-selection-fn show-action-sheet
:tabs {:tab-shadows? false}
:chats {:action-button? false
:new-chat-in-toolbar? true}
:contacts {:action-button? false
:new-contact-in-toolbar? true
:uppercase-subtitles? true
:group-block-shadows? false}
:discover {:uppercase-subtitles? true}})
{:component-styles component-styles
:fonts fonts
:list-selection-fn show-action-sheet
:tabs {:tab-shadows? false}
:chats {:action-button? false
:new-chat-in-toolbar? true}
:contacts {:action-button? false
:new-contact-in-toolbar? true
:uppercase-subtitles? true
:group-block-shadows? false}
:discover {:uppercase-subtitles? true}
:public-group-icon-container {:margin-top 2}
:private-group-icon-container {:margin-top 2}
:public-group-chat-hash-style {:top 6 :left 3}})

View File

@ -7,7 +7,9 @@
[clojure.string :as s]
[status-im.utils.handlers :as u]
[status-im.utils.random :as random]
[taoensso.timbre :refer-macros [debug]]))
[taoensso.timbre :refer-macros [debug]]
[taoensso.timbre :as log]
[status-im.navigation.handlers :as nav]))
(defn deselect-contact
[db [_ id]]
@ -44,7 +46,7 @@
:group-chat true
:group-admin current-public-key
:is-active true
:timestamp (.getTime (js/Date.))
:timestamp (random/timestamp)
:contacts contacts})))
(defn add-chat
@ -91,6 +93,42 @@
((after show-chat!))
((after start-listen-group!))))
(register-handler :create-new-public-group
(after (fn [_ [_ topic]]
(dispatch [:navigation-replace :chat topic])))
(u/side-effect!
(fn [db [_ topic]]
(let [exists? (boolean (get-in db [:chats topic]))
group {:chat-id topic
:name topic
:color default-chat-color
:group-chat true
:public? true
:is-active true
:timestamp (random/timestamp)}]
(when-not exists?
(dispatch [::add-public-group group])
(dispatch [::save-public-group group])
(dispatch [::start-watching-group topic]))))))
(register-handler ::add-public-group
(fn [db [_ {:keys [chat-id] :as group}]]
(assoc-in db [:chats chat-id] group)))
(register-handler ::save-public-group
(u/side-effect!
(fn [_ [_ group]]
(chats/save group))))
(register-handler ::start-watching-group
(u/side-effect!
(fn [{:keys [web3 current-public-key]} [_ topic]]
(protocol/start-watching-group!
{:web3 web3
:group-id topic
:identity current-public-key
:callback #(dispatch [:incoming-message %1 %2])}))))
(register-handler :group-chat-invite-received
(u/side-effect!
(fn [{:keys [current-public-key web3]}
@ -122,3 +160,7 @@
:identity current-public-key
:keypair keypair
:callback #(dispatch [:incoming-message %1 %2])})))))))
(defmethod nav/preload-data! :new-public-group
[db]
(dissoc db :public-group/topic))

View File

@ -1,4 +1,4 @@
(ns status-im.new-group.screen
(ns status-im.new-group.screen-private
(:require-macros [status-im.utils.views :refer [defview]])
(:require [re-frame.core :refer [subscribe dispatch]]
[status-im.resources :as res]

View File

@ -0,0 +1,65 @@
(ns status-im.new-group.screen-public
(:require-macros [status-im.utils.views :refer [defview]])
(:require [re-frame.core :refer [subscribe dispatch]]
[status-im.resources :as res]
[status-im.components.react :refer [view
text
image
icon
touchable-highlight
list-view
list-item]]
[status-im.components.text-field.view :refer [text-field]]
[status-im.components.styles :refer [color-blue
separator-color]]
[status-im.components.status-bar :refer [status-bar]]
[status-im.components.toolbar.view :refer [toolbar]]
[status-im.utils.listview :refer [to-datasource]]
[status-im.new-group.views.contact :refer [new-group-contact]]
[status-im.new-group.styles :as st]
[status-im.new-group.validations :as v]
[status-im.i18n :refer [label]]
[cljs.spec :as s]))
(defview new-group-toolbar []
[topic [:get :public-group/topic]]
(let [create-btn-enabled? (s/valid? ::v/topic topic)]
[view
[status-bar]
[toolbar
{:title (label :t/new-public-group-chat)
:actions [{:image {:source res/v ;; {:uri "icon_search"}
:style (st/toolbar-icon create-btn-enabled?)}
:handler (when create-btn-enabled?
#(dispatch [:create-new-public-group topic]))}]}]]))
(defview group-name-input []
[topic [:get :public-group/topic]]
[view
[text-field
{:error (cond
(not (s/valid? ::v/not-empty-string topic))
(label :t/empty-topic)
(not (s/valid? ::v/topic topic))
(label :t/topic-format))
:wrapper-style st/group-chat-name-wrapper
:error-color color-blue
:line-color separator-color
:label-hidden? true
:input-style st/group-chat-topic-input
:auto-focus true
:on-change-text #(dispatch [:set :public-group/topic %])
:value topic
:validator #(re-matches #"[a-z\-]*" %)
:auto-capitalize :none}]
[text {:style st/topic-hash} "#"]])
(defn new-public-group []
[view st/new-group-container
[new-group-toolbar]
[view st/chat-name-container
[text {:style st/members-text
:font :medium}
(label :t/public-group-topic)]
[group-name-input]]])

View File

@ -2,7 +2,8 @@
(:require [status-im.components.styles :refer [color-white
color-blue
text1-color
text2-color]]))
text2-color]]
[status-im.utils.platform :refer [platform-specific]]))
(defn toolbar-icon [enabled?]
{:width 20
@ -18,8 +19,20 @@
{:margin-left 16})
(def group-chat-name-input
{:font-size 14
:color text1-color})
{:font-size 14
:color text1-color})
(def group-chat-topic-input
{:font-size 14
:color text1-color
:padding-left 13})
(def topic-hash
(merge group-chat-name-input
{:width 10
:height 16
:position :absolute}
(get-in platform-specific [:public-group-chat-hash-style])))
(def group-chat-name-wrapper
{:padding-top 0})
@ -52,10 +65,10 @@
{:background-color :white})
(def contact-container
{:flex-direction :row
{:flex-direction :row
:justify-content :center
:align-items :center
:height 56})
:align-items :center
:height 56})
(def contact-item-checkbox
{:outer-size 20

View File

@ -15,3 +15,7 @@
(s/def ::name (s/and ::not-empty-string
::not-illegal-name))
(s/def ::topic (s/and ::not-empty-string
::not-illegal-name
(partial re-matches #"[a-z0-9\-]+")))

View File

@ -8,6 +8,7 @@
[status-im.protocol.web3.utils :as u]
[status-im.protocol.chat :as chat]
[status-im.protocol.group :as group]
[status-im.protocol.web3.public-group :as public-group]
[status-im.protocol.listeners :as l]
[status-im.protocol.encryption :as e]
[status-im.protocol.discoveries :as discoveries]
@ -25,6 +26,7 @@
(def start-watching-group! group/start-watching-group!)
(def stop-watching-group! group/stop-watching-group!)
(def send-group-message! group/send!)
(def send-public-group-message! group/send-to-public-group!)
(def invite-to-group! group/invite!)
(def update-group! group/update-group!)
(def remove-from-group! group/remove-identity!)
@ -51,7 +53,11 @@
(s/def ::rpc-url string?)
(s/def ::identity string?)
(s/def :message/chat-id string?)
(s/def ::group (s/keys :req-un [:message/chat-id :message/keypair]))
(s/def ::public? (s/and boolean? true?))
(s/def ::group-id :message/chat-id)
(s/def ::group (s/or
:group (s/keys :req-un [::group-id :message/keypair])
:public-group (s/keys :req-un [::group-id ::public?])))
(s/def ::groups (s/* ::group))
(s/def ::callback fn?)
(s/def ::contact (s/keys :req-un [::identity :message/keypair]))
@ -76,20 +82,18 @@
(d/reset-all-pending-messages!)
(let [web3 (u/make-web3 rpc-url)
listener-options {:web3 web3
:identity identity}]
:identity identity
:callback callback}]
;; start listening to groups
(doseq [{:keys [chat-id keypair]} groups]
(f/add-filter!
web3
{:topics [chat-id]}
(l/message-listener (assoc listener-options :callback callback
:keypair keypair))))
(doseq [group groups]
(let [options (merge listener-options group)]
(group/start-watching-group! options)))
;; start listening to user's inbox
(f/add-filter!
web3
{:to identity
:topics [f/status-topic]}
(l/message-listener (assoc listener-options :callback callback)))
(l/message-listener listener-options))
;; start listening to profiles
(doseq [{:keys [identity keypair]} contacts]
(watch-user! {:web3 web3
@ -98,10 +102,10 @@
:callback callback}))
(d/set-pending-mesage-callback! callback)
(let [online-message #(discoveries/send-online!
{:web3 web3
:message {:from identity
:message-id (random/id)
:keypair profile-keypair}})]
{:web3 web3
:message {:from identity
:message-id (random/id)
:keypair profile-keypair}})]
(d/run-delivery-loop!
web3
(assoc options :online-message online-message)))

View File

@ -6,22 +6,23 @@
[taoensso.timbre :refer-macros [debug]]
[status-im.protocol.validation :refer-macros [valid?]]
[status-im.protocol.web3.filtering :as f]
[status-im.protocol.listeners :as l]))
[status-im.protocol.listeners :as l]
[clojure.string :as str]))
(defn prepare-mesage
[{:keys [message group-id keypair new-keypair type]}]
[{:keys [message group-id keypair new-keypair type username requires-ack?]}]
(let [message' (-> message
(update :payload assoc
:username username
:group-id group-id
:type type
:timestamp (u/timestamp))
(assoc :topics [group-id]
:requires-ack? true
:keypair keypair
:requires-ack? (or (nil? requires-ack?) requires-ack?)
:type type))]
(if new-keypair
(assoc message' :new-keypair keypair)
message')))
(cond-> message'
keypair (assoc :keypair keypair)
new-keypair (assoc :new-keypair keypair))))
(defn- send-group-message!
[{:keys [web3] :as opts} type]
@ -31,15 +32,25 @@
(debug :send-group-message message)
(d/add-pending-message! web3 message)))
(s/def ::group-message
(s/def :group/message
(s/merge :protocol/message (s/keys :req-un [:chat-message/payload])))
(s/def :public-group/username (s/and string? (complement str/blank?)))
(s/def :public-group/message
(s/merge :group/message (s/keys :username :public-group/username)))
(defn send!
[{:keys [keypair message] :as options}]
{:pre [(valid? :message/keypair keypair)
(valid? ::group-message message)]}
(valid? :group/message message)]}
(send-group-message! options :group-message))
(defn send-to-public-group!
[{:keys [message] :as options}]
{:pre [(valid? :public-group/message message)]}
(send-group-message! (assoc options :requires-ack? false)
:public-group-message))
(defn leave!
[options]
(send-group-message! options :leave-group))

View File

@ -35,7 +35,7 @@
:ack-not-received-s-interval 125
:default-ttl 120
:send-online-s-interval 180
:ttl {}
:ttl-config {:public-group-message 2400}
:max-attempts-number 3
:delivery-loop-ms-interval 500
:profile-keypair {:public updates-public-key
@ -79,10 +79,12 @@
(register-handler :check-sync
(u/side-effect!
(fn [{:keys [web3] :as db}]
(.getSyncing
(.-eth web3)
(fn [error sync]
(dispatch [:update-sync-state error sync]))))))
(if web3
(.getSyncing
(.-eth web3)
(fn [error sync]
(dispatch [:update-sync-state error sync])))
(s/execute-later #(dispatch [:check-sync]) (s/s->ms 10))))))
(register-handler :initialize-sync-listener
(fn [{:keys [sync-listening-started] :as db} _]
@ -107,6 +109,7 @@
(case type
:message (dispatch [:received-protocol-message! message])
:group-message (dispatch [:received-protocol-message! message])
:public-group-message (dispatch [:received-protocol-message! message])
:ack (if (#{:message :group-message} (:type payload))
(dispatch [:message-delivered message])
(dispatch [:pending-message-remove message]))
@ -417,8 +420,8 @@
(u/side-effect!
(fn [_ [_ error]]
(.log js/console error)
(let [message (.-message error)
ios-error? (re-find (re-pattern "Could not connect to the server.") message)
(let [message (.-message error)
ios-error? (re-find (re-pattern "Could not connect to the server.") message)
android-error? (re-find (re-pattern "Failed to connect") message)]
(when (or ios-error? android-error?)
(when android-error? (status/init-jail))

View File

@ -19,7 +19,7 @@
(s/def :payload/new-keypair :message/keypair)
(s/def :group-message/type
#{:group-message :group-invitation :add-group-identity
#{:public-group-message :group-message :group-invitation :add-group-identity
:remove-group-identity :leave-group :update-group})
(s/def :discover-message/type #{:online :status :discover :contact-request :update-keys})

View File

@ -0,0 +1,7 @@
(ns status-im.protocol.web3.public-group
(:require [status-im.protocol.web3.filtering :as f]
[status-im.protocol.listeners :as l]
[status-im.protocol.validation :refer-macros [valid?]]
[status-im.protocol.web3.delivery :as d]
[cljs.spec :as s]))

View File

@ -27,6 +27,7 @@
:members-active {:one "1 member, 1 active"
:other "{{count}} members, {{count}} active"
:zero "no members"}
:public-group-status "Public"
:active-online "Online"
:active-unknown "Unknown"
:available "Available"
@ -119,6 +120,10 @@
:chats "Chats"
:new-chat "New chat"
:new-group-chat "New group chat"
:new-public-group-chat "Join public group chat"
:empty-topic "Empty topic"
:topic-format "Wrong format [a-z0-9\\-]+"
:public-group-topic "Topic"
;discover
:discover "Discover"