move messages main screen to status-im2 (#14491)

* move messages main screen to status-im2
This commit is contained in:
flexsurfer 2022-12-05 14:22:06 +01:00 committed by GitHub
parent f7af7ca25d
commit da0f0d3a81
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 222 additions and 228 deletions

View File

@ -200,7 +200,7 @@ status-im.utils.platform/isMacOs?
status-im.utils.platform/isNix?
status-im.utils.platform/isWin?
status-im.utils.platform/android-version>=
status-im.utils.debounce/clear-all
utils.debounce/clear-all
status-im.transport.db/create-chat
status-im.utils.priority-map/priority-map
status-im.utils.priority-map/priority-map-by

View File

@ -3,7 +3,9 @@
[quo2.foundations.colors :as colors]
[quo2.components.icon :as icons]
[quo2.components.markdown.text :as text]
[clojure.string :as string]))
[clojure.string :as string]
[quo2.components.buttons.button :as button]
[quo2.components.avatars.user-avatar :as user-avatar]))
(def ^:private centrify-style
{:display :flex
@ -12,8 +14,6 @@
(def ^:private align-left (assoc centrify-style :align-items :flex-start))
(def ^:private icon-styles (assoc centrify-style :width 32 :height 32 :border-radius 10))
(defn- big? [size] (= size :big))
(defn- icon-props [color size]
@ -28,23 +28,18 @@
{:color color}
{:no-color true})))
(defn render-left-section [{:keys [on-press icon
icon-color icon-background-color]}
(defn left-section-view [{:keys [on-press icon accessibility-label type] :or {type :grey}}
put-middle-section-on-left?]
[rn/view {:style (merge
icon-styles
{:background-color icon-background-color
:width 32
:height 32}
{:accessibility-label accessibility-label}
(when put-middle-section-on-left? {:margin-right 5}))}
[rn/touchable-opacity {:on-press on-press}
[icons/icon icon (icon-props icon-color :big)]]])
[button/button {:on-press on-press :icon true :type type :size 32}
icon]])
(defn- mid-section-comp
[{:keys [description-img description-user-icon horizontal-description?
text-secondary-color align-mid? text-color icon main-text type description]}]
[rn/view {:style (assoc
centrify-style
[rn/view {:style (assoc centrify-style
:flex-direction :row
:margin-horizontal 2)}
(when (or (and (not horizontal-description?)
@ -77,12 +72,13 @@
horizontal-description? (assoc :margin-left 4 :margin-top 2))}
description])]])
(defn- render-mid-section
(defn- mid-section-view
[{:keys [horizontal-description? one-icon-align-left? type left-align?
main-text right-icon main-text-icon-color left-icon] :as props}]
main-text right-icon main-text-icon-color left-icon on-press avatar] :as props}]
(let [text-color (if (colors/dark?) colors/neutral-5 colors/neutral-95)
text-secondary-color (if (colors/dark?) colors/neutral-40 colors/neutral-50)
component-instance [mid-section-comp (assoc props :text-secondary-color text-secondary-color)]]
[rn/touchable-opacity {:on-press on-press}
[rn/view {:style (merge
(if left-align?
align-left
@ -95,6 +91,13 @@
:weight :semi-bold
:style {:color text-color}}
main-text]
:user-avatar [rn/view {:style (assoc centrify-style :flex-direction :row)}
[user-avatar/user-avatar avatar]
[text/text {:size :paragraph-1
:weight :semi-bold
:style {:padding-horizontal 4
:color text-color}}
main-text]]
:text-with-two-icons [rn/view {:style (assoc centrify-style :flex-direction :row)}
[icons/icon left-icon
(icon-props main-text-icon-color :big)]
@ -119,33 +122,19 @@
(when horizontal-description?
[icons/icon left-icon
(icon-props main-text-icon-color :big)])])]
:text-with-description component-instance)]))
:text-with-description component-instance)]]))
(defn- right-section-icon
[{:keys [background-color icon icon-color push-to-the-left?] :or {push-to-the-left? false}}]
[rn/view {:style (assoc
icon-styles
:background-color background-color
:width 32
:height 32
:margin-right (if push-to-the-left? 8 0))}
[icons/icon icon (icon-props icon-color :big)]])
(defn- render-right-section [right-section-buttons]
[rn/view {:style (assoc
centrify-style
(defn- right-section-view [right-section-buttons]
[rn/view {:style (assoc centrify-style
:flex-direction :row
:flex 1
:justify-content :flex-end)}
(let [last-icon-index (-> right-section-buttons count dec)]
(map-indexed (fn [index {:keys [icon background-color icon-color on-press]}]
(map-indexed (fn [index {:keys [icon on-press type] :or {type :grey}}]
^{:key index}
[rn/touchable-opacity {:on-press on-press}
[right-section-icon
{:icon icon
:background-color background-color
:icon-color icon-color
:push-to-the-left? (not= index last-icon-index)}]])
[rn/view {:style {:margin-right (if (not= index last-icon-index) 8 0)}}
[button/button {:on-press on-press :icon true :type type :size 32}
icon]])
right-section-buttons))])
(defn page-nav
@ -157,7 +146,7 @@
:page-nav-color color
:page-nav-background-uri image-uri
:mid-section
{:type one-of :text-only :text-with-two-icons :text-with-one-icon :text-with-description
{:type one-of :text-only :text-with-two-icons :text-with-one-icon :text-with-description :user-avatar
:icon icon
:main-text string
:left-icon icon
@ -170,13 +159,15 @@
:main-text-icon-color color
}
:left-section
{:on-press event
{:type button-type
:on-press event
:icon icon
:icon-color color
:icon-background-color color
}
:right-section-buttons vector of
{:on-press event
{:type button-type
:on-press event
:icon icon
:icon-color color
:icon-background-color color
@ -199,21 +190,23 @@
:one-icon-align-left? one-icon-align-left?
:right-icon (:right-icon mid-section)
:icon (:icon mid-section)
:left-icon (:left-icon mid-section)}]
:left-icon (:left-icon mid-section)
:avatar (:avatar mid-section)}]
[rn/view {:style (cond-> {:display :flex
:flex-direction :row
;; iPhone 11 Pro's height in Figma divided by Component height 56/1125
:align-items :center
:padding-horizontal 20
:height 56
:justify-content :space-between}
page-nav-background-uri (assoc :background-color page-nav-color)
page-nav-color (assoc :background page-nav-background-uri))}
[rn/view {:style {:flex 1
:flex-direction :row
:align-items :center}}
(render-left-section left-section put-middle-section-on-left?)
[left-section-view left-section put-middle-section-on-left?]
(when put-middle-section-on-left?
[render-mid-section (assoc mid-section-props
[mid-section-view (assoc mid-section-props
:left-align? true
:description (:description mid-section)
:description-color (:description-color mid-section)
@ -221,5 +214,5 @@
:align-mid? align-mid?
:description-user-icon (:description-user-icon mid-section))])]
(when-not put-middle-section-on-left?
[render-mid-section mid-section-props])
(render-right-section right-section-buttons)]))
[mid-section-view mid-section-props])
[right-section-view right-section-buttons]]))

View File

@ -3,7 +3,8 @@
["react-native" :as react-native]
["@react-native-community/blur" :as blur]
[react-native.flat-list :as flat-list]
[react-native.section-list :as section-list]))
[react-native.section-list :as section-list]
[react-native.platform :as platform]))
(def app-state ^js (.-AppState ^js react-native))
(def blur-view (reagent/adapt-react-class (.-BlurView blur)))
@ -55,3 +56,17 @@
(js->clj (.get (.-Dimensions ^js react-native) "window") :keywordize-keys true))
(def status-bar (.-StatusBar ^js react-native))
(defn hw-back-add-listener [callback]
(.addEventListener (.-BackHandler ^js react-native) "hardwareBackPress" callback))
(defn hw-back-remove-listener [callback]
(.removeEventListener (.-BackHandler ^js react-native) "hardwareBackPress" callback))
(def keyboard-avoiding-view-class (reagent/adapt-react-class (.-KeyboardAvoidingView react-native)))
(defn keyboard-avoiding-view [props & children]
(into [keyboard-avoiding-view-class
(merge (when platform/ios? {:behavior :padding})
props)]
children))

View File

@ -22,7 +22,7 @@
[status-im.bottom-sheet.core :as bottom-sheet]
[status-im.browser.webview-ref :as webview-ref]
["eth-phishing-detect" :as eth-phishing-detect]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.browser.eip3085 :as eip3085]
[status-im.browser.eip3326 :as eip3326]))

View File

@ -11,7 +11,7 @@
[status-im.ui.components.list.views :as list]
[status-im.ui.components.react :as react]
[status-im.ui.components.topbar :as topbar]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.utils.utils :as utils]
[reagent.core :as reagent]
[quo.react-native :as rn]

View File

@ -5,7 +5,7 @@
[status-im.ui.components.icons.icons :as icons]
[status-im.ui.components.react :as react]
[status-im.ui.screens.browser.styles :as styles]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.network.core :as network]
[quo.design-system.colors :as colors]
[quo.core :as quo])

View File

@ -15,7 +15,7 @@
[status-im.ui.screens.browser.permissions.views :as permissions.views]
[status-im.ui.screens.browser.site-blocked.views :as site-blocked.views]
[status-im.ui.screens.browser.styles :as styles]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.utils.http :as http]
[status-im.utils.js-resources :as js-res]
[status-im.ui.components.permissions :as components.permissions]

View File

@ -8,7 +8,7 @@
[status-im.i18n.i18n :as i18n]
[status-im.ui.components.list-selection :as list-selection]
[quo.design-system.colors :as colors]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.utils.platform :as platform])
(:require-macros [status-im.utils.views :refer [defview letsubs]]))

View File

@ -8,7 +8,7 @@
[status-im.i18n.i18n :as i18n]
[quo.core :as quo]
[status-im.ui.screens.chat.stickers.styles :as styles]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.ui.components.fast-image :as fast-image]))
(def icon-size 28)

View File

@ -35,7 +35,7 @@
[status-im.utils.platform :as platform]
[status-im.utils.utils :as utils]
[status-im.ui.screens.chat.sheets :as sheets]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im2.navigation.state :as navigation.state]
[status-im.react-native.resources :as resources]))

View File

@ -11,7 +11,7 @@
[status-im.ui.components.react :as react]
[status-im.ui.screens.communities.membership :as memberships]
[status-im.ui.components.icons.icons :as icons]
[status-im.utils.debounce :as debounce]))
[utils.debounce :as debounce]))
(def max-name-length 30)
(def max-description-length 140)

View File

@ -4,7 +4,7 @@
[status-im.ui.components.toolbar :as toolbar]
[status-im.ui.components.react :as react]
[quo.core :as quo]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.i18n.i18n :as i18n]
[clojure.string :as str]
[status-im.ui.components.list.views :as list]

View File

@ -5,7 +5,7 @@
[status-im.i18n.i18n :as i18n]
[status-im.ui.components.toolbar :as toolbar]
[status-im.communities.core :as communities]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.utils.handlers :refer [>evt <sub]]
[status-im.ui.screens.communities.create :as create]
[status-im.ui.components.keyboard-avoid-presentation :as kb-presentation]

View File

@ -4,7 +4,7 @@
[status-im.i18n.i18n :as i18n]
[status-im.ui.components.toolbar :as toolbar]
[status-im.communities.core :as communities]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.utils.handlers :refer [<sub]]
[status-im.ui.screens.communities.create-channel :as create-channel]))

View File

@ -3,7 +3,7 @@
[status-im.ui.components.toolbar :as toolbar]
[quo.core :as quo]
[status-im.i18n.i18n :as i18n]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.ui.components.list.views :as list]
[reagent.core :as reagent]
[status-im.utils.handlers :refer [>evt <sub]]

View File

@ -20,7 +20,7 @@
[status-im.ui.screens.chat.message.message :as message]
[status-im.ui.screens.chat.photos :as photos]
[status-im.ui.screens.profile.components.views :as profile.components]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[clojure.string :as string]
[status-im.ethereum.tokens :as tokens]
[quo.core :as quo]

View File

@ -17,7 +17,7 @@
[status-im.ui.components.topbar :as topbar]
[status-im.ui.screens.group.styles :as styles]
[quo.core :as quo]
[status-im.utils.debounce :as debounce])
[utils.debounce :as debounce])
(:require-macros [status-im.utils.views :as views]))
(defn- render-contact [row]

View File

@ -15,7 +15,7 @@
[status-im.add-new.core :as new-chat]
[status-im.ui.components.search-input.view :as search-input]
[status-im.add-new.db :as db]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.utils.utils :as utils]
[status-im.ui.components.topbar :as topbar]
[status-im.ui.components.plus-button :as components.plus-button]

View File

@ -6,7 +6,7 @@
[status-im.ui.screens.network.styles :as st]
[status-im.ui.screens.network.views :as network-settings]
[status-im.ui.components.react :as react]
[status-im.utils.debounce :refer [dispatch-and-chill]])
[utils.debounce :refer [dispatch-and-chill]])
(:require-macros [status-im.utils.views :as views]))
(views/defview network-details []

View File

@ -7,7 +7,7 @@
[status-im.ui.screens.onboarding.styles :as styles]
[status-im.utils.gfycat.core :as gfy]
[status-im.utils.identicon :as identicon]
[status-im.utils.debounce :refer [dispatch-and-chill]]
[utils.debounce :refer [dispatch-and-chill]]
[quo.core :as quo]
[status-im.utils.utils :as utils]
[status-im.ui.screens.onboarding.views :as ui]

View File

@ -8,7 +8,7 @@
[quo.core :as quo]
[status-im.utils.datetime :as datetime]
[status-im.ui.screens.onboarding.views :as ui]
[status-im.utils.debounce :refer [dispatch-and-chill]]
[utils.debounce :refer [dispatch-and-chill]]
[status-im.utils.utils :as utils]
[reagent.core :as reagent]))

View File

@ -2,7 +2,7 @@
(:require [re-frame.core :as re-frame]
[status-im.i18n.i18n :as i18n]
[status-im.ui.components.react :as react]
[status-im.utils.debounce :refer [dispatch-and-chill]]
[utils.debounce :refer [dispatch-and-chill]]
[quo.core :as quo]
[status-im.ui.screens.onboarding.views :as ui])
(:require-macros [status-im.utils.views :refer [defview letsubs]]))

View File

@ -16,7 +16,7 @@
[status-im.ui.screens.chat.message.message :as message]
[status-im.ui.screens.chat.photos :as photos]
[status-im.ui.screens.chat.utils :as chat.utils]
[status-im.utils.debounce :as debounce])
[utils.debounce :as debounce])
(:require-macros [status-im.utils.views :refer [defview letsubs]]))
(defn member-sheet [chat-id member us-admin?]

View File

@ -7,7 +7,7 @@
[status-im.ui.components.topbar :as topbar]
[quo.core :as quo]
[quo.design-system.colors :as colors]
[status-im.utils.debounce :as debounce]
[utils.debounce :as debounce]
[status-im.ui.components.search-input.view :as search-input]
[status-im.ui.screens.wallet.components.views :as components]
[status-im.ui.components.icons.icons :as icons]

View File

@ -59,7 +59,7 @@
:bottom (- 2 (:bottom insets))})
(defn bottom-sheet-background [window-height]
{:pointerEvents :none
{:pointer-events :none
:position :absolute
:left 0
:right 0

View File

@ -1,100 +0,0 @@
(ns status-im.ui2.screens.chat.view
(:require [reagent.core :as reagent]
[status-im.ui.components.connectivity.view :as connectivity]
[status-im.ui.components.topbar :as topbar]
[status-im.ui2.screens.chat.composer.view :as composer]
[status-im.utils.debounce :as debounce]
[quo.react-native :as rn]
[re-frame.core :as re-frame]
[status-im.i18n.i18n :as i18n]
[quo2.components.buttons.button :as quo2.button]
[quo2.foundations.colors :as colors]
[status-im.ui.components.react :as react]
[status-im2.navigation.state :as navigation.state]
[status-im.ui2.screens.chat.messages.view :as messages]
[status-im.utils.handlers :refer [<sub >evt]]
[status-im.ui.components.icons.icons :as icons]
[status-im.ui2.screens.chat.messages.pinned-message :as pinned-message]
[re-frame.db]
[quo2.components.navigation.floating-shell-button :as floating-shell-button]
[status-im.ui2.screens.chat.messages.message :as message]))
(defn topbar-content []
(let [window-width (<sub [:dimensions/window-width])
{:keys [group-chat chat-id chat-name]} (<sub [:chats/current-chat])]
[rn/view {:flex-direction :row :align-items :center :height 56}
[rn/touchable-highlight {:on-press #(when-not group-chat
(debounce/dispatch-and-chill [:chat.ui/show-profile chat-id] 1000))
:style {:flex 1 :margin-left 12 :width (- window-width 120)}}
[rn/view
[rn/text chat-name]]]]))
(defn back-button []
[quo2.button/button {:type :grey
:size 32
:width 32
:accessibility-label "back-button"
:on-press #(do
(>evt [:close-chat])
(>evt [:navigate-back]))}
[icons/icon :main-icons/arrow-left {:color (colors/theme-colors colors/neutral-100 colors/white)}]])
(defn search-button []
[quo2.button/button {:type :grey
:size 32
:width 32
:accessibility-label "search-button"}
[icons/icon :main-icons/search {:color (colors/theme-colors colors/neutral-100 colors/white)}]])
(defn navigate-back-handler []
(when (and (not @navigation.state/curr-modal) (= (get @re-frame.db/app-db :view-id) :chat))
(react/hw-back-remove-listener navigate-back-handler)
(>evt [:close-chat])
(>evt [:navigate-back])))
(defn chat-render []
(let [{:keys [chat-id show-input?] :as chat}
;;we want to react only on these fields, do not use full chat map here
(<sub [:chats/current-chat-chat-view])
mutual-contact-requests-enabled? (<sub [:mutual-contact-requests/enabled?])]
[react/keyboard-avoiding-view-new {:style {:flex 1}
:ignore-offset false}
;;TODO It is better to not use topbar component because of performance
[topbar/topbar {:navigation :none
:left-component [rn/view {:flex-direction :row :margin-left 16}
[back-button]]
:title-component [topbar-content]
:right-component [rn/view {:flex-direction :row :margin-right 16}
[search-button]]
:border-bottom false
:new-ui? true}]
[connectivity/loading-indicator]
;;TODO not implemented
#_(when chat-id
(if group-chat
[invitation-requests chat-id admins]
(when-not mutual-contact-requests-enabled? [add-contact-bar chat-id])))
[pinned-message/pin-limit-popover chat-id message/pinned-messages-list]
[message/pinned-banner chat-id]
;;MESSAGES LIST
[messages/messages-view
{:chat chat
:mutual-contact-requests-enabled? mutual-contact-requests-enabled?
:show-input? show-input?
:bottom-space 15}]
;;INPUT COMPOSER
(when show-input?
[composer/composer chat-id])
[floating-shell-button/floating-shell-button
{:jump-to {:on-press #(re-frame/dispatch [:shell/navigate-to-jump-to])
:label (i18n/label :t/jump-to)}}
{:position :absolute
:bottom 117}]]))
(defn chat []
(reagent/create-class
{:component-did-mount (fn []
(react/hw-back-remove-listener navigate-back-handler)
(react/hw-back-add-listener navigate-back-handler))
:component-will-unmount (fn [] (react/hw-back-remove-listener navigate-back-handler))
:reagent-render chat-render}))

View File

@ -2,7 +2,7 @@
(:require [re-frame.core :as re-frame]
[re-frame.interceptor :refer [->interceptor get-coeffect]]
[taoensso.timbre :as log]
[status-im.utils.debounce :as debounce]))
[utils.debounce :as debounce]))
(defn- pretty-print-event [ctx]
(let [[first _] (get-coeffect ctx :event)]

View File

@ -0,0 +1,86 @@
(ns status-im2.contexts.chat.messages.view
(:require [reagent.core :as reagent]
[re-frame.db]
[i18n.i18n :as i18n]
[react-native.core :as rn]
[utils.re-frame :as rf]
[utils.debounce :as debounce]
[quo2.core :as quo]
[status-im2.navigation.state :as navigation.state]
;;TODO move to status-im2
[status-im.ui2.screens.chat.composer.view :as composer]
[status-im.ui2.screens.chat.messages.view :as messages]
[status-im.ui2.screens.chat.messages.pinned-message :as pinned-message]
[status-im.ui2.screens.chat.messages.message :as message]))
(defn navigate-back-handler []
(when (and (not @navigation.state/curr-modal) (= (get @re-frame.db/app-db :view-id) :chat))
(rn/hw-back-remove-listener navigate-back-handler)
(rf/dispatch [:close-chat])
(rf/dispatch [:navigate-back])))
(defn page-nav []
(let [{:keys [group-chat chat-id chat-name emoji]} (rf/sub [:chats/current-chat])
display-name (first (rf/sub [:contacts/contact-two-names-by-identity chat-id]))
online? (rf/sub [:visibility-status-updates/online? chat-id])
contact (when-not group-chat (rf/sub [:contacts/contact-by-address chat-id]))
photo-path (when-not (empty? (:images contact)) (rf/sub [:chats/photo-path chat-id]))]
[quo/page-nav
{:align-mid? true
:mid-section
(if group-chat
{:type :text-only
:main-text (str emoji " " chat-name)}
{:type :user-avatar
:avatar {:full-name display-name
:online? online?
:profile-picture photo-path
:size :medium}
:main-text display-name
:on-press #(debounce/dispatch-and-chill [:chat.ui/show-profile chat-id] 1000)})
:left-section
{:on-press #(do
(rf/dispatch [:close-chat])
(rf/dispatch [:navigate-back]))
:icon :i/arrow-left
:accessibility-label :back-button}
:right-section-buttons
[{:on-press #() ;; TODO not implemented
:icon :i/options
:accessibility-label :options-button}]}]))
(defn chat-render []
(let [;;we want to react only on these fields, do not use full chat map here
{:keys [chat-id show-input?] :as chat} (rf/sub [:chats/current-chat-chat-view])
mutual-contact-requests-enabled? (rf/sub [:mutual-contact-requests/enabled?])]
[rn/keyboard-avoiding-view {:style {:flex 1}}
[page-nav]
[pinned-message/pin-limit-popover chat-id message/pinned-messages-list]
[message/pinned-banner chat-id]
;;MESSAGES LIST
[messages/messages-view
{:chat chat
:mutual-contact-requests-enabled? mutual-contact-requests-enabled?
:show-input? show-input?
:bottom-space 15}]
;;INPUT COMPOSER
(when show-input?
[composer/composer chat-id])
[quo/floating-shell-button
{:jump-to {:on-press #(rf/dispatch [:shell/navigate-to-jump-to])
:label (i18n/label :t/jump-to)}}
{:position :absolute
:bottom 117}]]))
(defn chat []
(reagent/create-class
{:component-did-mount (fn []
(rn/hw-back-remove-listener navigate-back-handler)
(rn/hw-back-add-listener navigate-back-handler))
:component-will-unmount (fn [] (rn/hw-back-remove-listener navigate-back-handler))
:reagent-render chat-render}))

View File

@ -4,9 +4,9 @@
[status-im2.contexts.communities.overview.view :as communities.overview]
[status-im2.contexts.shell.view :as shell]
[status-im2.contexts.quo-preview.main :as quo.preview]
[status-im2.contexts.chat.messages.view :as chat]
;; TODO remove when not used anymore\
[status-im.ui2.screens.chat.view :as chat]
;; TODO remove when not used anymore
[status-im.ui.screens.screens :as old-screens]))
(def components

View File

@ -1,4 +1,4 @@
(ns status-im.utils.debounce
(ns utils.debounce
(:require [re-frame.core :as re-frame]))
(def timeout (atom {}))