Bläddra i källkod

feat(Chat) Improve responsive behaviour further.

* Add buttons to send messages/set nickname.
* Redesign message/nickname inputs.
* Pin messages to the input.
* Add keyboard avoider for Safari.
* Make chat content scrollable on mobile.
master
Mihai-Andrei Uscat 4 år sedan
förälder
incheckning
43761fc398
Inget konto är kopplat till bidragsgivarens mejladress

+ 58
- 1
css/_atlaskit_overrides.scss Visa fil

@@ -1,3 +1,21 @@
1
+/**
2
+ * Mixins that mimic the way Atlaskit fills the screen with modals at low screen widths.
3
+ */
4
+@mixin full-size-modal-positioner() {
5
+    height: 100%;
6
+    left: 0;
7
+    position: fixed;
8
+    top: 0;
9
+    max-width: 100%;
10
+    width: 100%;
11
+}
12
+
13
+@mixin full-size-modal-dialog() {
14
+    height: 100%;
15
+    max-height: 100%;
16
+    border-radius: 0;
17
+}
18
+
1 19
 /**
2 20
  * Move the @atlaskit/flag container up a little bit so it does not cover the
3 21
  * toolbar with the first notification.
@@ -56,4 +74,43 @@
56 74
  .toolbox-button-wth-dialog > div:nth-child(2) {
57 75
     max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 46px);
58 76
     overflow-y: auto;
59
-}
77
+}
78
+
79
+/**
80
+ * The following selectors keep the chat modal full-size anywhere between 100px
81
+ * and 580px for desktop or 680px for mobile.
82
+ */
83
+@media (min-width: 100px) and (max-width: 320px) {
84
+    .smiley-input {
85
+        display: none;
86
+    }
87
+    .shift-right .focus-lock > div > div {
88
+        @include full-size-modal-positioner();
89
+    }
90
+
91
+    .shift-right .focus-lock [role="dialog"] {
92
+        @include full-size-modal-dialog();
93
+    }
94
+}
95
+
96
+@media (min-width: 480px) and (max-width: 580px) {
97
+    .shift-right .focus-lock > div > div {
98
+        @include full-size-modal-positioner();
99
+    }
100
+
101
+    .shift-right .focus-lock [role="dialog"] {
102
+        @include full-size-modal-dialog();
103
+    }
104
+}
105
+
106
+@media (min-width: 580px) and (max-width: 680px) {
107
+    .mobile-browser {
108
+        &.shift-right .focus-lock > div > div {
109
+            @include full-size-modal-positioner();
110
+        }
111
+
112
+        &.shift-right .focus-lock [role="dialog"] {
113
+            @include full-size-modal-dialog();
114
+        }
115
+    }
116
+}

+ 104
- 5
css/_chat.scss Visa fil

@@ -32,6 +32,13 @@
32 32
     width: $sidebarWidth;
33 33
     word-wrap: break-word;
34 34
 
35
+    display: flex;
36
+    flex-direction: column;
37
+
38
+    & > :first-child {
39
+        margin-top: auto;
40
+    }
41
+
35 42
     a {
36 43
         display: block;
37 44
     }
@@ -122,16 +129,61 @@
122 129
     }
123 130
 }
124 131
 
132
+.chat-input-container {
133
+    padding: 0 16px 24px;
134
+
135
+    &.populated {
136
+        #chat-input {
137
+            border: 1px solid #619CF4;
138
+
139
+            .send-button {
140
+                background: #1B67EC;
141
+                cursor: pointer;
142
+
143
+                path {
144
+                    fill: #fff;
145
+                }
146
+            }
147
+        }
148
+    }
149
+}
150
+
125 151
 #chat-input {
126
-    border-top: 1px solid $chatInputSeparatorColor;
152
+    border: 1px solid $chatInputSeparatorColor;
127 153
     display: flex;
128 154
     padding: 5px 10px;
155
+    border-radius: 3px;
129 156
 
130 157
     * {
131 158
         background-color: transparent;
132 159
     }
133 160
 }
134 161
 
162
+.send-button-container {
163
+    display: flex;
164
+    align-items: center;
165
+}
166
+
167
+.send-button {
168
+    display: flex;
169
+    align-items: center;
170
+    justify-content: center;
171
+    height: 40px;
172
+    width: 40px;
173
+    border-radius: 3px;
174
+
175
+    path {
176
+        fill: $chatInputSeparatorColor;
177
+    }
178
+}
179
+
180
+.mobile-browser {
181
+    .send-button {
182
+        height: 48px;
183
+        width: 48px;
184
+    }
185
+}
186
+
135 187
 .remoteuser {
136 188
     color: #B8C7E0;
137 189
 }
@@ -161,10 +213,47 @@
161 213
 #nickname {
162 214
     text-align: center;
163 215
     color: #9d9d9d;
164
-    font-size: 18px;
165
-    margin-top: 30px;
166
-    left: 5px;
167
-    right: 5px;
216
+    font-size: 16px;
217
+    margin: auto 0;
218
+    padding: 0 16px;
219
+
220
+    input {
221
+        height: 40px;
222
+    }
223
+
224
+    label {
225
+        line-height: 24px;
226
+    }
227
+
228
+    .enter-chat {
229
+        display: flex;
230
+        align-items: center;
231
+        justify-content: center;
232
+        margin-top: 16px;
233
+        height: 40px;
234
+        background: #1B67EC;
235
+        border-radius: 3px;
236
+        color: #fff;
237
+        cursor: pointer;
238
+
239
+        &.disabled {
240
+            color: #757575;
241
+            background: #11336E;
242
+            pointer-events: none;
243
+        }
244
+    }
245
+}
246
+
247
+.mobile-browser {
248
+    #nickname {
249
+        input {
250
+            height: 48px;
251
+        }
252
+
253
+        .enter-chat {
254
+            height: 48px;
255
+        }
256
+    }
168 257
 }
169 258
 
170 259
 .sideToolbarContainer {
@@ -411,6 +500,16 @@
411 500
     #chatconversation {
412 501
         width: 100%;
413 502
     }
503
+
504
+    .chat-input-container {
505
+        padding: 0 0 24px;
506
+    }
507
+}
508
+
509
+.touchmove-hack {
510
+    display: flex;
511
+    flex: 1;
512
+    overflow: auto;
414 513
 }
415 514
 
416 515
 /**

+ 0
- 37
css/_responsive.scss Visa fil

@@ -53,21 +53,6 @@
53 53
     }
54 54
 }
55 55
 
56
-@mixin full-size-modal-positioner() {
57
-    height: 100%;
58
-    left: 0;
59
-    position: fixed;
60
-    top: 0;
61
-    max-width: 100%;
62
-    width: 100%;
63
-}
64
-
65
-@mixin full-size-modal-dialog() {
66
-    height: 100%;
67
-    max-height: 100%;
68
-    border-radius: 0;
69
-}
70
-
71 56
 @media only screen and (max-width: $verySmallScreen) {
72 57
     .welcome {
73 58
         display: block;
@@ -165,25 +150,3 @@
165 150
         }
166 151
     }
167 152
 }
168
-
169
-@media (min-width: 480px) and (max-width: 580px) {
170
-    .shift-right .focus-lock > div > div {
171
-        @include full-size-modal-positioner();
172
-    }
173
-
174
-    .shift-right .focus-lock [role="dialog"] {
175
-        @include full-size-modal-dialog();
176
-    }
177
-}
178
-
179
-@media (min-width: 580px) and (max-width: 680px) {
180
-    .mobile-browser {
181
-        &.shift-right .focus-lock > div > div {
182
-            @include full-size-modal-positioner();
183
-        }
184
-
185
-        &.shift-right .focus-lock [role="dialog"] {
186
-            @include full-size-modal-dialog();
187
-        }
188
-    }
189
-}

+ 1
- 0
lang/main.json Visa fil

@@ -61,6 +61,7 @@
61 61
         "today": "Today"
62 62
     },
63 63
     "chat": {
64
+        "enter": "Enter chat room",
64 65
         "error": "Error: your message was not sent. Reason: {{error}}",
65 66
         "fieldPlaceHolder": "Type your message here",
66 67
         "messagebox": "Type a message",

+ 9
- 0
react/features/base/dialog/components/web/StatelessDialog.js Visa fil

@@ -53,6 +53,11 @@ type Props = {
53 53
      */
54 54
     hideCancelButton: boolean,
55 55
 
56
+    /**
57
+     * If true, no footer will be displayed.
58
+     */
59
+    disableFooter?: boolean,
60
+
56 61
     i18n: Object,
57 62
 
58 63
     /**
@@ -174,6 +179,10 @@ class StatelessDialog extends Component<Props> {
174 179
             this._renderCancelButton()
175 180
         ].filter(Boolean);
176 181
 
182
+        if (this.props.disableFooter) {
183
+            return null;
184
+        }
185
+
177 186
         return (
178 187
             <ModalFooter showKeyline = { propsFromModalFooter.showKeyline } >
179 188
                 {

+ 10
- 0
react/features/base/environment/utils.js Visa fil

@@ -11,6 +11,16 @@ export function isMobileBrowser() {
11 11
     return Platform.OS === 'android' || Platform.OS === 'ios';
12 12
 }
13 13
 
14
+
15
+/**
16
+ * Returns whether or not the current environment is an ios mobile device.
17
+ *
18
+ * @returns {boolean}
19
+ */
20
+export function isIosMobileBrowser() {
21
+    return Platform.OS === 'ios';
22
+}
23
+
14 24
 /**
15 25
  * Checks whether the chrome extensions defined in the config file are installed or not.
16 26
  *

+ 1
- 0
react/features/base/icons/svg/index.js Visa fil

@@ -73,6 +73,7 @@ export { default as IconOpenInNew } from './open_in_new.svg';
73 73
 export { default as IconOutlook } from './office365.svg';
74 74
 export { default as IconPhone } from './phone.svg';
75 75
 export { default as IconPin } from './enlarge.svg';
76
+export { default as IconPlane } from './paper-plane.svg';
76 77
 export { default as IconPresentation } from './presentation.svg';
77 78
 export { default as IconRaisedHand } from './raised-hand.svg';
78 79
 export { default as IconRec } from './rec.svg';

+ 3
- 0
react/features/base/icons/svg/paper-plane.svg Visa fil

@@ -0,0 +1,3 @@
1
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6667 1.66663L1.66669 10.8333L7.6326 11.8276L8.33335 17.0833L10.644 13.2323L16.6667 17.9166V1.66663ZM8.73722 10.3221L6.35041 9.92426L15 4.63839V14.5089L11.3161 11.6436L12.5 7.49996L8.73722 10.3221Z" fill="white"/>
3
+</svg>

+ 8
- 3
react/features/chat/components/web/Chat.js Visa fil

@@ -14,8 +14,10 @@ import ChatDialog from './ChatDialog';
14 14
 import Header from './ChatDialogHeader';
15 15
 import ChatInput from './ChatInput';
16 16
 import DisplayNameForm from './DisplayNameForm';
17
+import KeyboardAvoider from './KeyboardAvoider';
17 18
 import MessageContainer from './MessageContainer';
18 19
 import MessageRecipient from './MessageRecipient';
20
+import TouchmoveHack from './TouchmoveHack';
19 21
 
20 22
 /**
21 23
  * React Component for holding the chat feature in a side panel that slides in
@@ -112,13 +114,16 @@ class Chat extends AbstractChat<Props> {
112 114
     _renderChat() {
113 115
         return (
114 116
             <>
115
-                <MessageContainer
116
-                    messages = { this.props._messages }
117
-                    ref = { this._messageContainerRef } />
117
+                <TouchmoveHack isModal = { this.props._isModal }>
118
+                    <MessageContainer
119
+                        messages = { this.props._messages }
120
+                        ref = { this._messageContainerRef } />
121
+                </TouchmoveHack>
118 122
                 <MessageRecipient />
119 123
                 <ChatInput
120 124
                     onResize = { this._onChatInputResize }
121 125
                     onSend = { this._onSendMessage } />
126
+                <KeyboardAvoider />
122 127
             </>
123 128
         );
124 129
     }

+ 1
- 0
react/features/chat/components/web/ChatDialog.js Visa fil

@@ -24,6 +24,7 @@ function ChatDialog({ children }: Props) {
24 24
         <Dialog
25 25
             customHeader = { Header }
26 26
             disableEnter = { true }
27
+            disableFooter = { true }
27 28
             hideCancelButton = { true }
28 29
             submitDisabled = { true }
29 30
             titleKey = 'chat.title'>

+ 51
- 28
react/features/chat/components/web/ChatInput.js Visa fil

@@ -6,6 +6,7 @@ import TextareaAutosize from 'react-textarea-autosize';
6 6
 import type { Dispatch } from 'redux';
7 7
 
8 8
 import { translate } from '../../../base/i18n';
9
+import { Icon, IconPlane } from '../../../base/icons';
9 10
 import { connect } from '../../../base/redux';
10 11
 
11 12
 import SmileysPanel from './SmileysPanel';
@@ -81,6 +82,7 @@ class ChatInput extends Component<Props, State> {
81 82
         this._onDetectSubmit = this._onDetectSubmit.bind(this);
82 83
         this._onMessageChange = this._onMessageChange.bind(this);
83 84
         this._onSmileySelect = this._onSmileySelect.bind(this);
85
+        this._onSubmitMessage = this._onSubmitMessage.bind(this);
84 86
         this._onToggleSmileysPanel = this._onToggleSmileysPanel.bind(this);
85 87
         this._setTextAreaRef = this._setTextAreaRef.bind(this);
86 88
     }
@@ -109,30 +111,39 @@ class ChatInput extends Component<Props, State> {
109 111
             ? 'show-smileys' : 'hide-smileys'} smileys-panel`;
110 112
 
111 113
         return (
112
-            <div id = 'chat-input' >
113
-                <div className = 'smiley-input'>
114
-                    <div id = 'smileysarea'>
115
-                        <div id = 'smileys'>
116
-                            <Emoji
117
-                                onClick = { this._onToggleSmileysPanel }
118
-                                text = ':)' />
114
+            <div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
115
+                <div id = 'chat-input' >
116
+                    <div className = 'smiley-input'>
117
+                        <div id = 'smileysarea'>
118
+                            <div id = 'smileys'>
119
+                                <Emoji
120
+                                    onClick = { this._onToggleSmileysPanel }
121
+                                    text = ':)' />
122
+                            </div>
123
+                        </div>
124
+                        <div className = { smileysPanelClassName }>
125
+                            <SmileysPanel
126
+                                onSmileySelect = { this._onSmileySelect } />
119 127
                         </div>
120 128
                     </div>
121
-                    <div className = { smileysPanelClassName }>
122
-                        <SmileysPanel
123
-                            onSmileySelect = { this._onSmileySelect } />
129
+                    <div className = 'usrmsg-form'>
130
+                        <TextareaAutosize
131
+                            id = 'usermsg'
132
+                            inputRef = { this._setTextAreaRef }
133
+                            maxRows = { 5 }
134
+                            onChange = { this._onMessageChange }
135
+                            onHeightChange = { this.props.onResize }
136
+                            onKeyDown = { this._onDetectSubmit }
137
+                            placeholder = { this.props.t('chat.messagebox') }
138
+                            value = { this.state.message } />
139
+                    </div>
140
+                    <div className = 'send-button-container'>
141
+                        <div
142
+                            className = 'send-button'
143
+                            onClick = { this._onSubmitMessage }>
144
+                            <Icon src = { IconPlane } />
145
+                        </div>
124 146
                     </div>
125
-                </div>
126
-                <div className = 'usrmsg-form'>
127
-                    <TextareaAutosize
128
-                        id = 'usermsg'
129
-                        inputRef = { this._setTextAreaRef }
130
-                        maxRows = { 5 }
131
-                        onChange = { this._onMessageChange }
132
-                        onHeightChange = { this.props.onResize }
133
-                        onKeyDown = { this._onDetectSubmit }
134
-                        placeholder = { this.props.t('chat.messagebox') }
135
-                        value = { this.state.message } />
136 147
                 </div>
137 148
             </div>
138 149
         );
@@ -148,6 +159,24 @@ class ChatInput extends Component<Props, State> {
148 159
         this._textArea && this._textArea.focus();
149 160
     }
150 161
 
162
+
163
+    _onSubmitMessage: () => void;
164
+
165
+    /**
166
+     * Submits the message to the chat window.
167
+     *
168
+     * @returns {void}
169
+     */
170
+    _onSubmitMessage() {
171
+        const trimmed = this.state.message.trim();
172
+
173
+        if (trimmed) {
174
+            this.props.onSend(trimmed);
175
+
176
+            this.setState({ message: '' });
177
+        }
178
+
179
+    }
151 180
     _onDetectSubmit: (Object) => void;
152 181
 
153 182
     /**
@@ -163,13 +192,7 @@ class ChatInput extends Component<Props, State> {
163 192
             && event.shiftKey === false) {
164 193
             event.preventDefault();
165 194
 
166
-            const trimmed = this.state.message.trim();
167
-
168
-            if (trimmed) {
169
-                this.props.onSend(trimmed);
170
-
171
-                this.setState({ message: '' });
172
-            }
195
+            this._onSubmitMessage();
173 196
         }
174 197
     }
175 198
 

+ 11
- 1
react/features/chat/components/web/DisplayNameForm.js Visa fil

@@ -8,6 +8,8 @@ import { translate } from '../../../base/i18n';
8 8
 import { connect } from '../../../base/redux';
9 9
 import { updateSettings } from '../../../base/settings';
10 10
 
11
+import KeyboardAvoider from './KeyboardAvoider';
12
+
11 13
 /**
12 14
  * The type of the React {@code Component} props of {@DisplayNameForm}.
13 15
  */
@@ -70,16 +72,24 @@ class DisplayNameForm extends Component<Props, State> {
70 72
 
71 73
         return (
72 74
             <div id = 'nickname'>
73
-                <span>{ this.props.t('chat.nickname.title') }</span>
74 75
                 <form onSubmit = { this._onSubmit }>
75 76
                     <FieldTextStateless
76 77
                         autoFocus = { true }
78
+                        compact = { true }
77 79
                         id = 'nickinput'
80
+                        label = { t('chat.nickname.title') }
78 81
                         onChange = { this._onDisplayNameChange }
79 82
                         placeholder = { t('chat.nickname.popover') }
83
+                        shouldFitContainer = { true }
80 84
                         type = 'text'
81 85
                         value = { this.state.displayName } />
82 86
                 </form>
87
+                <div
88
+                    className = { `enter-chat${this.state.displayName.trim() ? '' : ' disabled'}` }
89
+                    onClick = { this._onSubmit }>
90
+                    { t('chat.enter') }
91
+                </div>
92
+                <KeyboardAvoider />
83 93
             </div>
84 94
         );
85 95
     }

+ 60
- 0
react/features/chat/components/web/KeyboardAvoider.js Visa fil

@@ -0,0 +1,60 @@
1
+// @flow
2
+
3
+import React, { useEffect, useState } from 'react';
4
+import styled from 'styled-components';
5
+
6
+import { isIosMobileBrowser } from '../../../base/environment/utils';
7
+
8
+const Avoider = styled.div`
9
+    height: ${props => props.elementHeight}px;
10
+`;
11
+
12
+/**
13
+ * Component that renders an element to lift the chat input above the Safari keyboard,
14
+ * computing the appropriate height comparisons based on the {@code visualViewport}.
15
+ *
16
+ * @returns {ReactElement}
17
+ */
18
+function KeyboardAvoider() {
19
+    if (!isIosMobileBrowser()) {
20
+        return null;
21
+    }
22
+
23
+    const [ elementHeight, setElementHeight ] = useState(0);
24
+    const [ storedHeight, setStoredHeight ] = useState(window.innerHeight);
25
+
26
+    /**
27
+     * Handles the resizing of the visual viewport in order to compute
28
+     * the {@code KeyboardAvoider}'s height.
29
+     *
30
+     * @returns {void}
31
+     */
32
+    function handleViewportResize() {
33
+        const { innerWidth, visualViewport: { width, height } } = window;
34
+
35
+        // Compare the widths to make sure the {@code visualViewport} didn't resize due to zooming.
36
+        if (width === innerWidth) {
37
+            if (height < storedHeight) {
38
+                setElementHeight(storedHeight - height);
39
+            } else {
40
+                setElementHeight(0);
41
+            }
42
+            setStoredHeight(height);
43
+        }
44
+    }
45
+
46
+    useEffect(() => {
47
+        // Call the handler in case the keyboard is open when the {@code KeyboardAvoider} is mounted.
48
+        handleViewportResize();
49
+
50
+        window.visualViewport.addEventListener('resize', handleViewportResize);
51
+
52
+        return () => {
53
+            window.visualViewport.removeEventListener('resize', handleViewportResize);
54
+        };
55
+    }, []);
56
+
57
+    return <Avoider elementHeight = { elementHeight } />;
58
+}
59
+
60
+export default KeyboardAvoider;

+ 67
- 0
react/features/chat/components/web/TouchmoveHack.js Visa fil

@@ -0,0 +1,67 @@
1
+// @flow
2
+
3
+import React, { useEffect, useRef } from 'react';
4
+
5
+import { isMobileBrowser } from '../../../base/environment/utils';
6
+
7
+type Props = {
8
+
9
+    /**
10
+     * The component(s) that need to be scrollable on mobile.
11
+     */
12
+   children: React$Node,
13
+
14
+    /**
15
+     * Whether the component is rendered within a modal.
16
+     */
17
+    isModal: boolean
18
+};
19
+
20
+
21
+/**
22
+ * Component that disables {@code touchmove} propagation below it.
23
+ *
24
+ * @returns {ReactElement}
25
+ */
26
+function TouchmoveHack({ children, isModal }: Props) {
27
+    if (!isModal || !isMobileBrowser()) {
28
+        return children;
29
+    }
30
+
31
+    const touchMoveElementRef = useRef(null);
32
+
33
+    /**
34
+     * Atlaskit's {@code Modal} uses a third party library to disable touchmove events
35
+     * which makes scrolling inside dialogs impossible. We therefore employ this hack
36
+     * to intercept and stop the propagation of touchmove events from this wrapper that
37
+     * is placed around the chat conversation from the {@code ChatDialog}.
38
+     *
39
+     * @param {Event} event - The touchmove event fired within the component.
40
+     * @returns {void}
41
+     */
42
+    function handleTouchMove(event: TouchEvent) {
43
+        event.stopImmediatePropagation();
44
+    }
45
+
46
+    useEffect(() => {
47
+        if (touchMoveElementRef && touchMoveElementRef.current) {
48
+            touchMoveElementRef.current.addEventListener('touchmove', handleTouchMove, true);
49
+        }
50
+
51
+        return () => {
52
+            if (touchMoveElementRef && touchMoveElementRef.current) {
53
+                touchMoveElementRef.current.removeEventListener('touchmove', handleTouchMove, true);
54
+            }
55
+        };
56
+    }, []);
57
+
58
+    return (
59
+        <div
60
+            className = 'touchmove-hack'
61
+            ref = { touchMoveElementRef }>
62
+            {children}
63
+        </div>
64
+    );
65
+}
66
+
67
+export default TouchmoveHack;

Laddar…
Avbryt
Spara