[fix 3424] visually indicated when mailserver or all peers are disconnected
Signed-off-by: Pedro Pombeiro <pombeirp@users.noreply.github.com>
This commit is contained in:
parent
eff794c2ff
commit
54546204ae
|
@ -15,7 +15,7 @@
|
||||||
[status-im.ui.components.list-selection :as list-selection]
|
[status-im.ui.components.list-selection :as list-selection]
|
||||||
[status-im.ui.components.react :as react]
|
[status-im.ui.components.react :as react]
|
||||||
[status-im.ui.components.status-bar.view :as status-bar]
|
[status-im.ui.components.status-bar.view :as status-bar]
|
||||||
[status-im.ui.components.sync-state.offline :as offline]
|
[status-im.ui.components.connectivity.view :as connectivity]
|
||||||
[status-im.ui.components.toolbar.view :as toolbar]
|
[status-im.ui.components.toolbar.view :as toolbar]
|
||||||
[status-im.utils.platform :as platform]))
|
[status-im.utils.platform :as platform]))
|
||||||
|
|
||||||
|
@ -103,4 +103,4 @@
|
||||||
[input/container {:text-empty? (string/blank? input-text)}]
|
[input/container {:text-empty? (string/blank? input-text)}]
|
||||||
(when show-bottom-info?
|
(when show-bottom-info?
|
||||||
[bottom-info/bottom-info-view])
|
[bottom-info/bottom-info-view])
|
||||||
[offline/offline-view {:top (get platform/platform-specific :status-bar-default-height)}]]))
|
[connectivity/error-view {:top (get platform/platform-specific :status-bar-default-height)}]]))
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
icon]]
|
icon]]
|
||||||
[status-im.chat.views.input.animations.expandable :refer [expandable-view]]
|
[status-im.chat.views.input.animations.expandable :refer [expandable-view]]
|
||||||
[status-im.chat.views.input.box-header :as box-header]
|
[status-im.chat.views.input.box-header :as box-header]
|
||||||
[status-im.ui.components.sync-state.offline :refer [offline-view]]))
|
[status-im.ui.components.connectivity.view :as connectivity]))
|
||||||
|
|
||||||
(defview result-box-container [markup]
|
(defview result-box-container [markup]
|
||||||
[view {:flex 1}
|
[view {:flex 1}
|
||||||
|
@ -20,4 +20,4 @@
|
||||||
[expandable-view {:key :result-box
|
[expandable-view {:key :result-box
|
||||||
:custom-header (box-header/get-header :result-box)}
|
:custom-header (box-header/get-header :result-box)}
|
||||||
[result-box-container markup]
|
[result-box-container markup]
|
||||||
[offline-view]]))
|
[connectivity/error-view]]))
|
||||||
|
|
|
@ -241,108 +241,143 @@
|
||||||
#(re-frame/dispatch [::request-messages-error %]))))
|
#(re-frame/dispatch [::request-messages-error %]))))
|
||||||
|
|
||||||
(re-frame/reg-fx
|
(re-frame/reg-fx
|
||||||
::handle-whisper-message
|
::handle-whisper-message
|
||||||
listeners/handle-whisper-message)
|
listeners/handle-whisper-message)
|
||||||
|
|
||||||
;;;; Handlers
|
;;;; Handlers
|
||||||
|
|
||||||
|
|
||||||
|
(defn get-wnode [db]
|
||||||
|
(let [wnode-id (get db :inbox/wnode)]
|
||||||
|
(get-in db [:inbox/wnodes wnode-id :address])))
|
||||||
|
|
||||||
|
(defn connectivity-check [peers {:keys [db] :as cofx}]
|
||||||
|
(let [wnode (get-wnode db)
|
||||||
|
peers-count (count peers)
|
||||||
|
mailserver-connected? (inbox/registered-peer? peers wnode)]
|
||||||
|
{:db (cond-> db
|
||||||
|
mailserver-connected? (dissoc :mailserver-status)
|
||||||
|
(not mailserver-connected?) (assoc :mailserver-status :disconnected)
|
||||||
|
:always (assoc :peers-count peers-count))}))
|
||||||
|
|
||||||
|
(re-frame/reg-fx
|
||||||
|
:connectivity/fetch-peers
|
||||||
|
(fn [{:keys [wnode web3 retries]}]
|
||||||
|
(inbox/fetch-peers #(re-frame/dispatch [:connectivity-check-success %])
|
||||||
|
#(log/error :connectivity/fetch-peers %))))
|
||||||
|
|
||||||
|
(handlers/register-handler-fx
|
||||||
|
:connectivity-check
|
||||||
|
(fn [{:keys [db]} _]
|
||||||
|
(let [web3 (:web3 db)
|
||||||
|
wnode (get-wnode db)]
|
||||||
|
{:connectivity/fetch-peers {:wnode wnode
|
||||||
|
:web3 web3}})))
|
||||||
|
|
||||||
|
(handlers/register-handler-fx
|
||||||
|
:connectivity-check-success
|
||||||
|
(fn [{:keys [db] :as cofx} [_ peers]]
|
||||||
|
(handlers/merge-fx cofx
|
||||||
|
{:dispatch-later [{:ms 30000 :dispatch [:connectivity-check]}]}
|
||||||
|
(connectivity-check peers))))
|
||||||
|
|
||||||
;; NOTE(dmitryn): events chain
|
;; NOTE(dmitryn): events chain
|
||||||
;; add-peer -> fetch-peers -> mark-trusted-peer -> get-sym-key -> request-messages
|
;; add-peer -> fetch-peers -> mark-trusted-peer -> get-sym-key -> request-messages
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
:initialize-offline-inbox
|
:initialize-offline-inbox
|
||||||
(fn [{:keys [db]} [_ web3]]
|
(fn [{:keys [db]} [_ web3]]
|
||||||
(log/info "offline inbox: initialize")
|
(log/info "offline inbox: initialize")
|
||||||
(let [wnode-id (get db :inbox/wnode)
|
(let [wnode (get-wnode db)]
|
||||||
wnode (get-in db [:inbox/wnodes wnode-id :address])]
|
{::add-peer {:wnode wnode
|
||||||
{::add-peer {:wnode wnode
|
:web3 web3}})))
|
||||||
:web3 web3}})))
|
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::add-peer-success
|
::add-peer-success
|
||||||
(fn [{:keys [db]} [_ web3 response]]
|
(fn [{:keys [db]} [_ web3 response]]
|
||||||
(let [wnode-id (get db :inbox/wnode)
|
(let [wnode (get-wnode db)]
|
||||||
wnode (get-in db [:inbox/wnodes wnode-id :address])]
|
(log/info "offline inbox: add-peer response" wnode response)
|
||||||
(log/info "offline inbox: add-peer response" wnode response)
|
{::fetch-peers {:wnode wnode
|
||||||
{::fetch-peers {:wnode wnode
|
:web3 web3
|
||||||
:web3 web3
|
:retries 0}})))
|
||||||
:retries 0}})))
|
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::fetch-peers-success
|
::fetch-peers-success
|
||||||
(fn [{:keys [db]} [_ web3 peers retries]]
|
(fn [{:keys [db] :as cofx} [_ web3 peers retries]]
|
||||||
(let [wnode-id (get db :inbox/wnode)
|
(let [wnode (get-wnode db)]
|
||||||
wnode (get-in db [:inbox/wnodes wnode-id :address])]
|
(log/info "offline inbox: fetch-peers response" peers)
|
||||||
(log/info "offline inbox: fetch-peers response" peers)
|
(if (inbox/registered-peer? peers wnode)
|
||||||
(if (inbox/registered-peer? peers wnode)
|
(handlers/merge-fx cofx
|
||||||
{::mark-trusted-peer {:wnode wnode
|
{::mark-trusted-peer {:wnode wnode
|
||||||
:web3 web3
|
:web3 web3
|
||||||
:peers peers}}
|
:peers peers}
|
||||||
(do
|
:dispatch-later [{:ms 30000 :dispatch [:connectivity-check]}]}
|
||||||
(log/info "Peer" wnode "is not registered. Retrying fetch peers.")
|
(connectivity-check peers))
|
||||||
{::fetch-peers {:wnode wnode
|
(do
|
||||||
:web3 web3
|
(log/info "Peer" wnode "is not registered. Retrying fetch peers.")
|
||||||
:retries (inc retries)}})))))
|
(handlers/merge-fx cofx
|
||||||
|
{::fetch-peers {:wnode wnode
|
||||||
|
:web3 web3
|
||||||
|
:retries (inc retries)}}
|
||||||
|
(connectivity-check peers)))))))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::mark-trusted-peer-success
|
::mark-trusted-peer-success
|
||||||
(fn [{:keys [db]} [_ web3 response]]
|
(fn [{:keys [db]} [_ web3 response]]
|
||||||
(let [wnode-id (get db :inbox/wnode)
|
(let [wnode (get-wnode db)
|
||||||
wnode (get-in db [:inbox/wnodes wnode-id :address])
|
password (:inbox/password db)]
|
||||||
password (:inbox/password db)]
|
(log/info "offline inbox: mark-trusted-peer response" wnode response)
|
||||||
(log/info "offline inbox: mark-trusted-peer response" wnode response)
|
{::get-sym-key {:password password
|
||||||
{::get-sym-key {:password password
|
:web3 web3}})))
|
||||||
:web3 web3}})))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::get-sym-key-success
|
::get-sym-key-success
|
||||||
(fn [{:keys [db]} [_ web3 sym-key-id]]
|
(fn [{:keys [db]} [_ web3 sym-key-id]]
|
||||||
(log/info "offline inbox: get-sym-key response" sym-key-id)
|
(log/info "offline inbox: get-sym-key response" sym-key-id)
|
||||||
(let [wnode-id (get db :inbox/wnode)
|
(let [wnode (get-wnode db)
|
||||||
wnode (get-in db [:inbox/wnodes wnode-id :address])
|
topic (:inbox/topic db)]
|
||||||
topic (:inbox/topic db)]
|
{::request-messages {:wnode wnode
|
||||||
{::request-messages {:wnode wnode
|
:topic topic
|
||||||
:topic topic
|
:sym-key-id sym-key-id
|
||||||
:sym-key-id sym-key-id
|
:web3 web3}})))
|
||||||
:web3 web3}})))
|
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::request-messages-success
|
::request-messages-success
|
||||||
(fn [_ [_ response]]
|
(fn [_ [_ response]]
|
||||||
(log/info "offline inbox: request-messages response" response)))
|
(log/info "offline inbox: request-messages response" response)))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::add-peer-error
|
::add-peer-error
|
||||||
(fn [_ [_ error]]
|
(fn [_ [_ error]]
|
||||||
(log/error "offline inbox: add-peer error" error)))
|
(log/error "offline inbox: add-peer error" error)))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::fetch-peers-error
|
::fetch-peers-error
|
||||||
(fn [_ [_ error]]
|
(fn [_ [_ error]]
|
||||||
(log/error "offline inbox: fetch-peers error" error)))
|
(log/error "offline inbox: fetch-peers error" error)))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::mark-trusted-peer-error
|
::mark-trusted-peer-error
|
||||||
(fn [_ [_ error]]
|
(fn [_ [_ error]]
|
||||||
(log/error "offline inbox: mark-trusted-peer error" error)))
|
(log/error "offline inbox: mark-trusted-peer error" error)))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::get-sym-key-error
|
::get-sym-key-error
|
||||||
(fn [_ [_ error]]
|
(fn [_ [_ error]]
|
||||||
(log/error "offline inbox: get-sym-key error" error)))
|
(log/error "offline inbox: get-sym-key error" error)))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
::request-messages-error
|
::request-messages-error
|
||||||
(fn [_ [_ error]]
|
(fn [_ [_ error]]
|
||||||
(log/error "offline inbox: request-messages error" error)))
|
(log/error "offline inbox: request-messages error" error)))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
:handle-whisper-message
|
:handle-whisper-message
|
||||||
(fn [_ [_ error msg options]]
|
(fn [_ [_ error msg options]]
|
||||||
{::handle-whisper-message {:error error
|
{::handle-whisper-message {:error error
|
||||||
:msg msg
|
:msg msg
|
||||||
:options options}}))
|
:options options}}))
|
||||||
|
|
||||||
;;; INITIALIZE PROTOCOL
|
;;; INITIALIZE PROTOCOL
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
|
@ -361,7 +396,7 @@
|
||||||
:updates-public-key updates-public-key :updates-private-key updates-private-key
|
:updates-public-key updates-public-key :updates-private-key updates-private-key
|
||||||
:status status :contacts all-contacts}
|
:status status :contacts all-contacts}
|
||||||
:db (assoc db :web3 web3
|
:db (assoc db :web3 web3
|
||||||
:rpc-url (or ethereum-rpc-url constants/ethereum-rpc-url))}))))
|
:rpc-url (or ethereum-rpc-url constants/ethereum-rpc-url))}))))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
:load-processed-messages
|
:load-processed-messages
|
||||||
|
@ -461,8 +496,8 @@
|
||||||
(when (nil? route-fx) (log/debug "Unknown message type" type))
|
(when (nil? route-fx) (log/debug "Unknown message type" type))
|
||||||
(cache/add! processed-message)
|
(cache/add! processed-message)
|
||||||
(merge
|
(merge
|
||||||
{::save-processed-messages processed-message}
|
{::save-processed-messages processed-message}
|
||||||
route-fx))))))
|
route-fx))))))
|
||||||
|
|
||||||
(handlers/register-handler-fx
|
(handlers/register-handler-fx
|
||||||
:update-message-status
|
:update-message-status
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
:chat-name "Chat name"
|
:chat-name "Chat name"
|
||||||
:notifications-title "Notifications and sounds"
|
:notifications-title "Notifications and sounds"
|
||||||
:offline "Offline"
|
:offline "Offline"
|
||||||
|
:connection-problem "Messages connection problem"
|
||||||
:search-for "Search for..."
|
:search-for "Search for..."
|
||||||
:cancel "Cancel"
|
:cancel "Cancel"
|
||||||
:next "Next"
|
:next "Next"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
(ns status-im.ui.components.sync-state.styles
|
(ns status-im.ui.components.connectivity.styles
|
||||||
(:require-macros [status-im.utils.styles :refer [defnstyle]]))
|
(:require-macros [status-im.utils.styles :refer [defnstyle]]))
|
||||||
|
|
||||||
(defnstyle offline-wrapper [top opacity window-width pending?]
|
(defnstyle offline-wrapper [top opacity window-width pending?]
|
|
@ -0,0 +1,41 @@
|
||||||
|
(ns status-im.ui.components.connectivity.view
|
||||||
|
(:require [re-frame.core :as re-frame]
|
||||||
|
[reagent.core :as reagent]
|
||||||
|
[status-im.ui.components.react :as react]
|
||||||
|
[status-im.ui.components.connectivity.styles :as styles]
|
||||||
|
[status-im.ui.components.animation :as animation]
|
||||||
|
[status-im.i18n :as i18n]))
|
||||||
|
|
||||||
|
(def window-width (:width (react/get-dimensions "window")))
|
||||||
|
|
||||||
|
(defn start-error-animation [offline-opacity]
|
||||||
|
(animation/start
|
||||||
|
(animation/timing offline-opacity {:toValue 1.0
|
||||||
|
:duration 250})))
|
||||||
|
|
||||||
|
(defn error-view [_]
|
||||||
|
(let [offline? (re-frame/subscribe [:offline?])
|
||||||
|
connection-problem? (re-frame/subscribe [:connection-problem?])
|
||||||
|
offline-opacity (animation/create-value 0.0)
|
||||||
|
on-update (fn [_ _]
|
||||||
|
(animation/set-value offline-opacity 0)
|
||||||
|
(when (or @offline? @connection-problem?)
|
||||||
|
(start-error-animation offline-opacity)))
|
||||||
|
pending-contact? (re-frame/subscribe [:current-contact :pending?])
|
||||||
|
view-id (re-frame/subscribe [:get :view-id])]
|
||||||
|
(reagent/create-class
|
||||||
|
{:component-did-mount
|
||||||
|
on-update
|
||||||
|
:component-did-update
|
||||||
|
on-update
|
||||||
|
:display-name "connectivity-error-view"
|
||||||
|
:reagent-render
|
||||||
|
(fn [{:keys [top]}]
|
||||||
|
(when (or @offline? @connection-problem?)
|
||||||
|
(let [pending? (and @pending-contact? (= :chat @view-id))]
|
||||||
|
[react/animated-view {:style (styles/offline-wrapper top offline-opacity window-width pending?)}
|
||||||
|
[react/view
|
||||||
|
[react/text {:style styles/offline-text}
|
||||||
|
(i18n/label (if @connection-problem?
|
||||||
|
:t/connection-problem
|
||||||
|
:t/offline))]]])))})))
|
|
@ -1,39 +0,0 @@
|
||||||
(ns status-im.ui.components.sync-state.offline
|
|
||||||
(:require [re-frame.core :as re-frame]
|
|
||||||
[reagent.core :as reagent]
|
|
||||||
[status-im.ui.components.react :as react]
|
|
||||||
[status-im.ui.components.sync-state.styles :as styles]
|
|
||||||
[status-im.ui.components.animation :as animation]
|
|
||||||
[status-im.i18n :as i18n]))
|
|
||||||
|
|
||||||
(def window-width (:width (react/get-dimensions "window")))
|
|
||||||
|
|
||||||
(defn start-offline-animation [offline-opacity]
|
|
||||||
(animation/start
|
|
||||||
(animation/timing offline-opacity {:toValue 1.0
|
|
||||||
:duration 250})))
|
|
||||||
|
|
||||||
(defn offline-view [_]
|
|
||||||
(let [sync-state (re-frame/subscribe [:sync-state])
|
|
||||||
network-status (re-frame/subscribe [:get :network-status])
|
|
||||||
offline-opacity (animation/create-value 0.0)
|
|
||||||
on-update (fn [_ _]
|
|
||||||
(animation/set-value offline-opacity 0)
|
|
||||||
(when (or (= @network-status :offline) (= @sync-state :offline))
|
|
||||||
(start-offline-animation offline-opacity)))
|
|
||||||
pending-contact? (re-frame/subscribe [:current-contact :pending?])
|
|
||||||
view-id (re-frame/subscribe [:get :view-id])]
|
|
||||||
(reagent/create-class
|
|
||||||
{:component-did-mount
|
|
||||||
on-update
|
|
||||||
:component-did-update
|
|
||||||
on-update
|
|
||||||
:display-name "offline-view"
|
|
||||||
:reagent-render
|
|
||||||
(fn [{:keys [top]}]
|
|
||||||
(when (or (= @network-status :offline) (= @sync-state :offline))
|
|
||||||
(let [pending? (and @pending-contact? (= :chat @view-id))]
|
|
||||||
[react/animated-view {:style (styles/offline-wrapper top offline-opacity window-width pending?)}
|
|
||||||
[react/view
|
|
||||||
[react/text {:style styles/offline-text}
|
|
||||||
(i18n/label :t/offline)]]])))})))
|
|
|
@ -68,6 +68,9 @@
|
||||||
;;:online - presence of internet connection in the phone
|
;;:online - presence of internet connection in the phone
|
||||||
(spec/def ::network-status (spec/nilable keyword?))
|
(spec/def ::network-status (spec/nilable keyword?))
|
||||||
|
|
||||||
|
(spec/def ::mailserver-status (spec/nilable keyword?))
|
||||||
|
(spec/def ::peers-count (spec/nilable integer?))
|
||||||
|
|
||||||
;;;;NODE
|
;;;;NODE
|
||||||
|
|
||||||
(spec/def ::sync-listening-started (spec/nilable boolean?))
|
(spec/def ::sync-listening-started (spec/nilable boolean?))
|
||||||
|
@ -174,6 +177,8 @@
|
||||||
::keyboard-max-height
|
::keyboard-max-height
|
||||||
::orientation
|
::orientation
|
||||||
::network-status
|
::network-status
|
||||||
|
::mailserver-status
|
||||||
|
::peers-count
|
||||||
::sync-listening-started
|
::sync-listening-started
|
||||||
::sync-state
|
::sync-state
|
||||||
::sync-data
|
::sync-data
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
[status-im.ui.components.native-action-button :refer [native-action-button]]
|
[status-im.ui.components.native-action-button :refer [native-action-button]]
|
||||||
[status-im.ui.components.toolbar.view :as toolbar]
|
[status-im.ui.components.toolbar.view :as toolbar]
|
||||||
[status-im.ui.components.toolbar.actions :as toolbar.actions]
|
[status-im.ui.components.toolbar.actions :as toolbar.actions]
|
||||||
[status-im.ui.components.sync-state.offline :refer [offline-view]]
|
[status-im.ui.components.connectivity.view :as connectivity]
|
||||||
[status-im.ui.screens.home.views.inner-item :as inner-item]
|
[status-im.ui.screens.home.views.inner-item :as inner-item]
|
||||||
[status-im.ui.screens.home.styles :as styles]
|
[status-im.ui.screens.home.styles :as styles]
|
||||||
[status-im.ui.components.icons.vector-icons :as vector-icons]
|
[status-im.ui.components.icons.vector-icons :as vector-icons]
|
||||||
|
@ -86,4 +86,4 @@
|
||||||
^{:key home-item-id} [home-list-deletable home-item])}])
|
^{:key home-item-id} [home-list-deletable home-item])}])
|
||||||
(when platform/android?
|
(when platform/android?
|
||||||
[home-action-button])
|
[home-action-button])
|
||||||
[offline-view]]))
|
[connectivity/error-view]]))
|
||||||
|
|
|
@ -31,13 +31,26 @@
|
||||||
(fn [current-account]
|
(fn [current-account]
|
||||||
(:signed-up? current-account)))
|
(:signed-up? current-account)))
|
||||||
|
|
||||||
(reg-sub :network
|
(reg-sub :network :network)
|
||||||
(fn [db]
|
|
||||||
(:network db)))
|
|
||||||
|
|
||||||
(reg-sub :sync-state
|
(reg-sub :sync-state :sync-state)
|
||||||
(fn [db]
|
(reg-sub :network-status :network-status)
|
||||||
(:sync-state db)))
|
(reg-sub :peers-count :peers-count)
|
||||||
|
(reg-sub :mailserver-status :mailserver-status)
|
||||||
|
|
||||||
|
(reg-sub :offline?
|
||||||
|
:<- [:network-status]
|
||||||
|
:<- [:sync-state]
|
||||||
|
(fn [[network-status sync-state]]
|
||||||
|
(or (= network-status :offline)
|
||||||
|
(= sync-state :offline))))
|
||||||
|
|
||||||
|
(reg-sub :connection-problem?
|
||||||
|
:<- [:mailserver-status]
|
||||||
|
:<- [:peers-count]
|
||||||
|
(fn [[mailserver-status peers-count]]
|
||||||
|
(or (= :disconnected mailserver-status)
|
||||||
|
(zero? peers-count))))
|
||||||
|
|
||||||
(reg-sub :syncing?
|
(reg-sub :syncing?
|
||||||
:<- [:sync-state]
|
:<- [:sync-state]
|
||||||
|
|
Loading…
Reference in New Issue