mirror of
https://github.com/status-im/status-mobile.git
synced 2025-01-27 00:49:39 +00:00
Support join.status.im links for adding a contact (#14964)
* Support join.status.im links for adding a contact (#14814) * refactor tests, add caret to regex
This commit is contained in:
parent
e61bd769a8
commit
7315dc8914
@ -1,5 +1,5 @@
|
||||
(ns status-im.integration-test
|
||||
(:require [cljs.test :refer [deftest is run-tests]]
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[clojure.string :as string]
|
||||
[day8.re-frame.test :as rf-test]
|
||||
[re-frame.core :as rf]
|
||||
@ -10,7 +10,7 @@
|
||||
[status-im.transport.core :as transport]
|
||||
[status-im.utils.test :as utils.test]
|
||||
status-im2.navigation.core
|
||||
status-im2.subs.root ;;so integration tests can run independently
|
||||
status-im2.subs.root ; so integration tests can run independently
|
||||
[taoensso.timbre :as log]
|
||||
[utils.security.core :as security]))
|
||||
|
||||
@ -107,9 +107,7 @@
|
||||
[::transport/messenger-started]
|
||||
(assert-messenger-started)
|
||||
(logout!)
|
||||
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not in
|
||||
; an
|
||||
; inconsistent state between tests
|
||||
(rf-test/wait-for [::logout/logout-method]
|
||||
(assert-logout)))))))
|
||||
|
||||
(deftest create-community-test
|
||||
@ -156,9 +154,7 @@
|
||||
[:wallet.accounts/account-stored]
|
||||
(assert-new-account-created) ; assert account was created
|
||||
(logout!)
|
||||
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not in
|
||||
; an
|
||||
; inconsistent state between tests
|
||||
(rf-test/wait-for [::logout/logout-method]
|
||||
(assert-logout))))))))
|
||||
|
||||
(deftest back-up-seed-phrase-test
|
||||
@ -191,9 +187,7 @@
|
||||
[:my-profile/finish-success]
|
||||
(is (nil? @(rf/subscribe [:mnemonic]))) ; assert seed phrase has been removed
|
||||
(logout!)
|
||||
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not
|
||||
; in
|
||||
; an inconsistent state between tests
|
||||
(rf-test/wait-for [::logout/logout-method]
|
||||
(assert-logout)))))))))
|
||||
|
||||
(def multiaccount-name "Narrow Frail Lemming")
|
||||
@ -226,9 +220,7 @@
|
||||
[::transport/messenger-started]
|
||||
(assert-messenger-started)
|
||||
(logout!)
|
||||
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not in
|
||||
; an
|
||||
; inconsistent state between tests
|
||||
(rf-test/wait-for [::logout/logout-method]
|
||||
(assert-logout))))))))
|
||||
|
||||
(def chat-id
|
||||
@ -254,9 +246,7 @@
|
||||
(rf/dispatch-sync [:chat/navigate-to-chat chat-id])
|
||||
(is (= chat-id @(rf/subscribe [:chats/current-chat-id])))
|
||||
(logout!)
|
||||
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not in
|
||||
; an
|
||||
; inconsistent state between tests
|
||||
(rf-test/wait-for [::logout/logout-method]
|
||||
(assert-logout))))))))
|
||||
|
||||
(deftest delete-chat-test
|
||||
@ -282,9 +272,7 @@
|
||||
(rf/dispatch-sync [:chat.ui/show-remove-confirmation chat-id])
|
||||
(rf/dispatch-sync [:chat.ui/remove-chat chat-id])
|
||||
(logout!)
|
||||
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is not
|
||||
; in an
|
||||
; inconsistent state between tests
|
||||
(rf-test/wait-for [::logout/logout-method]
|
||||
(assert-logout))))))))
|
||||
|
||||
(deftest mute-chat-test
|
||||
@ -316,10 +304,41 @@
|
||||
[:chat/mute-successfully]
|
||||
(is (not @(rf/subscribe [:chats/muted chat-id])))
|
||||
(logout!)
|
||||
(rf-test/wait-for [::logout/logout-method] ; we need to logout to make sure the node is
|
||||
; not in
|
||||
; an inconsistent state between tests
|
||||
(rf-test/wait-for [::logout/logout-method]
|
||||
(assert-logout))))))))))
|
||||
|
||||
(comment
|
||||
(run-tests))
|
||||
(deftest add-contact-test
|
||||
(log/info "========= add-contact-test ==================")
|
||||
(let
|
||||
[compressed-key "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA"
|
||||
public-key
|
||||
"0x048a6773339d11ccf5fd81677b7e54daeec544a1287bd92b725047ad6faa9a9b9f8ea86ed5a226d2a994f5f46d0b43321fd8de7b7997a166e67905c8c73cd37cea"
|
||||
three-words-name "Rich Total Pondskater"]
|
||||
(rf-test/run-test-async
|
||||
(initialize-app!)
|
||||
(rf-test/wait-for
|
||||
[:setup/initialize-view]
|
||||
(generate-and-derive-addresses!)
|
||||
(rf-test/wait-for
|
||||
[:multiaccount-generate-and-derive-addresses-success]
|
||||
(assert-multiaccount-loaded)
|
||||
(create-multiaccount!)
|
||||
(rf-test/wait-for
|
||||
[::transport/messenger-started]
|
||||
(assert-messenger-started)
|
||||
;; search for contact using compressed key
|
||||
(rf/dispatch [:contacts/set-new-identity compressed-key])
|
||||
(rf-test/wait-for
|
||||
[:contacts/set-new-identity-success]
|
||||
(let [new-identity @(rf/subscribe [:contacts/new-identity])]
|
||||
(is (= public-key (:public-key new-identity)))
|
||||
(is (= :valid (:state new-identity))))
|
||||
;; click 'view profile' button
|
||||
(rf/dispatch [:chat.ui/show-profile public-key])
|
||||
(rf-test/wait-for
|
||||
[:contacts/contact-built]
|
||||
(let [contact @(rf/subscribe [:contacts/current-contact])]
|
||||
(is (= three-words-name (:three-words-name (:names contact)))))
|
||||
(logout!)
|
||||
(rf-test/wait-for [::logout/logout-method]
|
||||
(assert-logout))))))))))
|
||||
|
@ -27,101 +27,102 @@
|
||||
|
||||
(def status
|
||||
(clj->js
|
||||
{:openAccounts (fn [callback]
|
||||
(callback
|
||||
(.openAccounts
|
||||
native-status
|
||||
test-dir)))
|
||||
:multiAccountStoreDerived (fn [json callback]
|
||||
(callback (.multiAccountStoreDerivedAccounts
|
||||
native-status
|
||||
json)))
|
||||
{:openAccounts
|
||||
(fn [callback]
|
||||
(callback (.openAccounts native-status test-dir)))
|
||||
|
||||
:multiAccountStoreDerived
|
||||
(fn [json callback]
|
||||
(callback (.multiAccountStoreDerivedAccounts native-status json)))
|
||||
|
||||
:clearCookies identity
|
||||
|
||||
:clearStorageAPIs identity
|
||||
|
||||
:setBlankPreviewFlag identity
|
||||
:callPrivateRPC (fn [payload callback]
|
||||
(callback
|
||||
(.callPrivateRPC
|
||||
native-status
|
||||
payload)))
|
||||
:saveAccountAndLogin (fn [multiaccount-data password settings config
|
||||
accounts-data]
|
||||
(.saveAccountAndLogin
|
||||
native-status
|
||||
multiaccount-data
|
||||
password
|
||||
settings
|
||||
config
|
||||
accounts-data))
|
||||
:logout (fn []
|
||||
(.logout native-status))
|
||||
:generateAlias (fn [seed]
|
||||
(.generateAlias native-status seed))
|
||||
:generateAliasAndIdenticonAsync (fn [seed callback]
|
||||
(let [generated-identicon (.identicon native-status seed)
|
||||
generated-alias (.generateAlias native-status
|
||||
seed)]
|
||||
(callback generated-alias generated-identicon)))
|
||||
:multiAccountGenerateAndDeriveAddresses (fn [json callback]
|
||||
(callback
|
||||
(.multiAccountGenerateAndDeriveAddresses
|
||||
native-status
|
||||
json)))
|
||||
:multiAccountImportMnemonic (fn [json callback]
|
||||
(callback
|
||||
(.multiAccountImportMnemonic
|
||||
native-status
|
||||
json)))
|
||||
:multiAccountLoadAccount (fn [json callback]
|
||||
(callback
|
||||
(.multiAccountLoadAccount
|
||||
native-status
|
||||
json)))
|
||||
:multiAccountDeriveAddresses (fn [json callback]
|
||||
(callback
|
||||
(.multiAccountDeriveAddresses
|
||||
native-status
|
||||
json)))
|
||||
:initKeystore (fn [key-uid callback]
|
||||
(callback
|
||||
(.initKeystore
|
||||
native-status
|
||||
(str test-dir "/keystore/" key-uid))))
|
||||
|
||||
:identicon (fn [pk]
|
||||
(.identicon native-status pk))
|
||||
:callPrivateRPC
|
||||
(fn [payload callback]
|
||||
(callback (.callPrivateRPC native-status payload)))
|
||||
|
||||
:encodeTransfer (fn [to-norm amount-hex]
|
||||
(.encodeTransfer native-status to-norm amount-hex))
|
||||
:saveAccountAndLogin
|
||||
(fn [multiaccount-data password settings config accounts-data]
|
||||
(.saveAccountAndLogin native-status multiaccount-data password settings config accounts-data))
|
||||
|
||||
:encodeFunctionCall (fn [method params-json]
|
||||
(.encodeFunctionCall native-status method params-json))
|
||||
:logout
|
||||
(fn [] (.logout native-status))
|
||||
|
||||
:decodeParameters (fn [decode-param-json]
|
||||
(.decodeParameters native-status decode-param-json))
|
||||
:generateAlias
|
||||
(fn [seed] (.generateAlias native-status seed))
|
||||
|
||||
:hexToNumber (fn [hex]
|
||||
(.hexToNumber native-status hex))
|
||||
:generateAliasAndIdenticonAsync
|
||||
(fn [seed callback]
|
||||
(let [generated-identicon (.identicon native-status seed)
|
||||
generated-alias (.generateAlias native-status seed)]
|
||||
(callback generated-alias generated-identicon)))
|
||||
|
||||
:numberToHex (fn [num-str]
|
||||
(.numberToHex native-status num-str))
|
||||
:multiAccountGenerateAndDeriveAddresses
|
||||
(fn [json callback]
|
||||
(callback (.multiAccountGenerateAndDeriveAddresses native-status json)))
|
||||
|
||||
:checkAddressChecksum (fn [address]
|
||||
(.checkAddressChecksum native-status address))
|
||||
:multiAccountImportMnemonic
|
||||
(fn [json callback]
|
||||
(callback (.multiAccountImportMnemonic native-status json)))
|
||||
|
||||
:sha3 (fn [str]
|
||||
(.sha3 native-status str))
|
||||
:multiAccountLoadAccount
|
||||
(fn [json callback]
|
||||
(callback (.multiAccountLoadAccount native-status json)))
|
||||
|
||||
:toChecksumAddress (fn [address]
|
||||
(.toChecksumAddress native-status address))
|
||||
:multiAccountDeriveAddresses
|
||||
(fn [json callback]
|
||||
(callback (.multiAccountDeriveAddresses native-status json)))
|
||||
|
||||
:isAddress (fn [address]
|
||||
(.isAddress native-status address))
|
||||
:multiformatDeserializePublicKey
|
||||
(fn [public-key deserialization-key callback]
|
||||
(callback (.multiformatDeserializePublicKey
|
||||
native-status
|
||||
public-key
|
||||
deserialization-key)))
|
||||
|
||||
:validateMnemonic (fn [json callback]
|
||||
(callback
|
||||
(.validateMnemonic
|
||||
native-status
|
||||
json)))
|
||||
:initKeystore
|
||||
(fn [key-uid callback]
|
||||
(callback (.initKeystore native-status
|
||||
(str test-dir "/keystore/" key-uid))))
|
||||
|
||||
:identicon
|
||||
(fn [pk] (.identicon native-status pk))
|
||||
|
||||
:encodeTransfer
|
||||
(fn [to-norm amount-hex]
|
||||
(.encodeTransfer native-status to-norm amount-hex))
|
||||
|
||||
:encodeFunctionCall
|
||||
(fn [method params-json]
|
||||
(.encodeFunctionCall native-status method params-json))
|
||||
|
||||
:decodeParameters
|
||||
(fn [decode-param-json]
|
||||
(.decodeParameters native-status decode-param-json))
|
||||
|
||||
:hexToNumber
|
||||
(fn [hex] (.hexToNumber native-status hex))
|
||||
|
||||
:numberToHex
|
||||
(fn [num-str] (.numberToHex native-status num-str))
|
||||
|
||||
:checkAddressChecksum
|
||||
(fn [address] (.checkAddressChecksum native-status address))
|
||||
|
||||
:sha3
|
||||
(fn [str] (.sha3 native-status str))
|
||||
|
||||
:toChecksumAddress
|
||||
(fn [address] (.toChecksumAddress native-status address))
|
||||
|
||||
:isAddress
|
||||
(fn [address] (.isAddress native-status address))
|
||||
|
||||
:validateMnemonic
|
||||
(fn [json callback] (callback (.validateMnemonic native-status json)))
|
||||
|
||||
:startLocalNotifications identity}))
|
||||
|
@ -9,11 +9,6 @@
|
||||
[status-im2.utils.validators :as validators]
|
||||
[status-im.utils.utils :as utils]))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:contacts/resolve-public-key-from-ens-name
|
||||
(fn [{:keys [chain-id ens-name on-success on-error]}]
|
||||
(ens/pubkey chain-id ens-name on-success on-error)))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:contacts/decompress-public-key
|
||||
(fn [{:keys [public-key on-success on-error]}]
|
||||
@ -23,50 +18,73 @@
|
||||
(let [{:keys [error]} (types/json->clj resp)]
|
||||
(if error
|
||||
(on-error error)
|
||||
(on-success
|
||||
(str "0x" (subs resp 5)))))))))
|
||||
(on-success (str "0x" (subs resp 5)))))))))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:contacts/resolve-public-key-from-ens-name
|
||||
(fn [{:keys [chain-id ens-name on-success on-error]}]
|
||||
(ens/pubkey chain-id ens-name on-success on-error)))
|
||||
|
||||
(defn fx-callbacks
|
||||
[input ens-name]
|
||||
{:on-success (fn [pubkey]
|
||||
(rf/dispatch
|
||||
[:contacts/set-new-identity-success
|
||||
input ens-name pubkey]))
|
||||
:on-error (fn [err]
|
||||
(rf/dispatch
|
||||
[:contacts/set-new-identity-error err input]))})
|
||||
|
||||
(defn identify-type
|
||||
[input]
|
||||
(let [regex #"^https?://join.status.im/u/(.+)"
|
||||
id (as-> (utils/safe-trim input) $
|
||||
(if-some [[_ match] (re-matches regex $)]
|
||||
match
|
||||
$)
|
||||
(if (empty? $) nil $))
|
||||
public-key? (validators/valid-public-key? id)
|
||||
compressed-key? (validators/valid-compressed-key? id)
|
||||
type (cond (empty? id) :empty
|
||||
public-key? :public-key
|
||||
compressed-key? :compressed-key
|
||||
:else :ens-name)
|
||||
ens-name (when (= type :ens-name)
|
||||
(stateofus/ens-name-parse id))]
|
||||
{:input input
|
||||
:id id
|
||||
:type type
|
||||
:ens-name ens-name}))
|
||||
|
||||
(rf/defn set-new-identity
|
||||
{:events [:contacts/set-new-identity]}
|
||||
[{:keys [db]} input]
|
||||
(let [input (utils/safe-trim input)
|
||||
public-key? (validators/valid-public-key? input)
|
||||
compressed-key? (validators/valid-compressed-key? input)
|
||||
ens-name (if compressed-key? nil (stateofus/ens-name-parse input))
|
||||
on-success (fn [pubkey]
|
||||
(rf/dispatch
|
||||
[:contacts/set-new-identity-success
|
||||
input ens-name pubkey]))
|
||||
on-error (fn [err]
|
||||
(rf/dispatch
|
||||
[:contacts/set-new-identity-error err input]))]
|
||||
(cond
|
||||
(empty? input) {:db (dissoc db :contacts/new-identity)}
|
||||
public-key? {:db (assoc db
|
||||
:contacts/new-identity
|
||||
{:input input
|
||||
:public-key input
|
||||
:ens-name nil
|
||||
:state :error
|
||||
:error :uncompressed-key})}
|
||||
:else
|
||||
(cond->
|
||||
{:db (assoc db
|
||||
:contacts/new-identity
|
||||
{:input input
|
||||
:public-key nil
|
||||
:ens-name nil
|
||||
:state :searching
|
||||
:error nil})}
|
||||
compressed-key? (assoc :contacts/decompress-public-key
|
||||
{:public-key input
|
||||
:on-success on-success
|
||||
:on-error on-error})
|
||||
(not compressed-key?) (assoc :contacts/resolve-public-key-from-ens-name
|
||||
{:chain-id (ethereum/chain-id db)
|
||||
:ens-name ens-name
|
||||
:on-success on-success
|
||||
:on-error on-error})))))
|
||||
(let [{:keys [input id type ens-name]} (identify-type input)]
|
||||
(case type
|
||||
:empty {:db (dissoc db :contacts/new-identity)}
|
||||
:public-key {:db (assoc db
|
||||
:contacts/new-identity
|
||||
{:input input
|
||||
:public-key id
|
||||
:state :error
|
||||
:error :uncompressed-key})}
|
||||
:compressed-key {:db
|
||||
(assoc db
|
||||
:contacts/new-identity
|
||||
{:input input
|
||||
:state :searching})
|
||||
:contacts/decompress-public-key
|
||||
(merge {:public-key id}
|
||||
(fx-callbacks id ens-name))}
|
||||
:ens-name {:db
|
||||
(assoc db
|
||||
:contacts/new-identity
|
||||
{:input input
|
||||
:state :searching})
|
||||
:contacts/resolve-public-key-from-ens-name
|
||||
(merge {:chain-id (ethereum/chain-id db)
|
||||
:ens-name ens-name}
|
||||
(fx-callbacks id ens-name))})))
|
||||
|
||||
(rf/defn set-new-identity-success
|
||||
{:events [:contacts/set-new-identity-success]}
|
||||
@ -76,19 +94,16 @@
|
||||
{:input input
|
||||
:public-key pubkey
|
||||
:ens-name ens-name
|
||||
:state :valid
|
||||
:error nil})})
|
||||
:state :valid})})
|
||||
|
||||
(rf/defn set-new-identity-error
|
||||
{:events [:contacts/set-new-identity-error]}
|
||||
[{:keys [db]} error input]
|
||||
{:db (assoc db
|
||||
:contacts/new-identity
|
||||
{:input input
|
||||
:public-key nil
|
||||
:ens-name nil
|
||||
:state :error
|
||||
:error :invalid})})
|
||||
{:input input
|
||||
:state :error
|
||||
:error :invalid})})
|
||||
|
||||
(rf/defn clear-new-identity
|
||||
{:events [:contacts/clear-new-identity :contacts/new-chat-focus]}
|
||||
|
@ -1,53 +1,56 @@
|
||||
(ns status-im2.contexts.add-new-contact.events-test
|
||||
(:require [cljs.test :refer-macros [deftest is]]
|
||||
(:require [cljs.test :refer-macros [deftest is are]]
|
||||
[status-im2.contexts.add-new-contact.events :as core]))
|
||||
|
||||
(def init-db
|
||||
{:networks/current-network "mainnet_rpc"
|
||||
:networks/networks {"mainnet_rpc"
|
||||
{:id "mainnet_rpc"
|
||||
:config {:NetworkId 1}}}})
|
||||
(def ukey
|
||||
"0x045596a7ff87da36860a84b0908191ce60a504afc94aac93c1abd774f182967ce694f1bf2d8773cd59f4dd0863e951f9b7f7351c5516291a0fceb73f8c392a0e88")
|
||||
(def ckey "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA")
|
||||
(def ens "esep")
|
||||
(def ens-stateofus-eth (str ens ".stateofus.eth"))
|
||||
(def link-ckey (str "https://join.status.im/u/" ckey))
|
||||
(def link-ens (str "https://join.status.im/u/" ens))
|
||||
|
||||
(defn search-db
|
||||
[input]
|
||||
{:contacts/new-identity {:input input
|
||||
:public-key nil
|
||||
:ens-name nil
|
||||
:state :searching
|
||||
:error nil}})
|
||||
(deftest identify-type-test
|
||||
(are [input expected] (= (core/identify-type input) expected)
|
||||
"" {:input ""
|
||||
:id nil
|
||||
:type :empty
|
||||
:ens-name nil}
|
||||
|
||||
(deftest search-empty-string
|
||||
(let [input ""
|
||||
expected {:db init-db}
|
||||
actual (core/set-new-identity {:db init-db} input)]
|
||||
(is (= actual expected))))
|
||||
ukey {:input ukey
|
||||
:id ukey
|
||||
:type :public-key
|
||||
:ens-name nil}
|
||||
|
||||
(deftest search-ens
|
||||
(let [input "esep"
|
||||
clean (fn [db]
|
||||
(-> db
|
||||
(assoc-in [:contacts/resolve-public-key-from-ens-name :on-success] nil)
|
||||
(assoc-in [:contacts/resolve-public-key-from-ens-name :on-error] nil)))
|
||||
expected {:db (merge init-db (search-db input))
|
||||
:contacts/resolve-public-key-from-ens-name
|
||||
{:chain-id 1
|
||||
:ens-name (str input ".stateofus.eth")
|
||||
:on-success nil
|
||||
:on-error nil}}
|
||||
actual (core/set-new-identity {:db init-db} input)]
|
||||
(is (= (clean actual) expected))))
|
||||
ens {:input ens
|
||||
:id ens
|
||||
:type :ens-name
|
||||
:ens-name ens-stateofus-eth}
|
||||
|
||||
(deftest search-compressed-key
|
||||
(let [input "zQ3shWj4WaBdf2zYKCkXe6PHxDxNTzZyid1i75879Ue9cX9gA"
|
||||
clean (fn [db]
|
||||
(-> db
|
||||
(assoc-in [:contacts/decompress-public-key :on-success] nil)
|
||||
(assoc-in [:contacts/decompress-public-key :on-error] nil)))
|
||||
expected {:db (merge init-db (search-db input))
|
||||
:contacts/decompress-public-key
|
||||
{:public-key input
|
||||
:on-success nil
|
||||
:on-error nil}}
|
||||
actual (core/set-new-identity {:db init-db} input)]
|
||||
(is (= (clean actual) expected))))
|
||||
ckey {:input ckey
|
||||
:id ckey
|
||||
:type :compressed-key
|
||||
:ens-name nil}
|
||||
|
||||
link-ckey {:input link-ckey
|
||||
:id ckey
|
||||
:type :compressed-key
|
||||
:ens-name nil}
|
||||
|
||||
link-ens {:input link-ens
|
||||
:id ens
|
||||
:type :ens-name
|
||||
:ens-name ens-stateofus-eth}))
|
||||
|
||||
(deftest search-empty-string-test
|
||||
(is (= (core/set-new-identity {:db {:contacts/new-identity :foo}} "")
|
||||
{:db {}})))
|
||||
|
||||
(deftest search-uncompressed-key-test
|
||||
(is (= (core/set-new-identity {:db {}} ukey)
|
||||
{:db {:contacts/new-identity
|
||||
{:input ukey
|
||||
:public-key ukey
|
||||
:state :error
|
||||
:error :uncompressed-key}}})))
|
||||
|
||||
|
@ -1875,7 +1875,7 @@
|
||||
"identity-verification-request": "Identity verification",
|
||||
"identity-verification-request-sent": "asks",
|
||||
"type-something": "Type something",
|
||||
"type-some-chat-key": "0x123abc",
|
||||
"type-some-chat-key": "zQ3abc...123",
|
||||
"your-answer": "Your answer",
|
||||
"membership": "Membership",
|
||||
"jump-to": "Jump to",
|
||||
|
Loading…
x
Reference in New Issue
Block a user