Skip to content

Commit e86f0ca

Browse files
authored
Merge pull request #9344 from nextcloud/feature/9271/typing-indicators
Typing indicators in chat [web client]
2 parents 11cce6f + f3a0c4f commit e86f0ca

File tree

9 files changed

+344
-23
lines changed

9 files changed

+344
-23
lines changed

src/components/AvatarWrapper/AvatarWrapper.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
'avatar-wrapper--offline': offline,
2626
'avatar-wrapper--small': small,
2727
'avatar-wrapper--condensed': condensed,
28-
}">
28+
}"
29+
:style="{'--condensed-overlap': condensedOverlap}">
2930
<div v-if="iconClass"
3031
class="icon"
3132
:class="[`avatar-${size}px`, iconClass]" />
@@ -80,6 +81,10 @@ export default {
8081
type: Boolean,
8182
default: false,
8283
},
84+
condensedOverlap: {
85+
type: Number,
86+
default: 2,
87+
},
8388
offline: {
8489
type: Boolean,
8590
default: false,
@@ -134,7 +139,7 @@ export default {
134139
if (this.isDeletedUser) {
135140
return 'X'
136141
}
137-
const customName = this.name !== t('spreed', 'Guest') ? this.name : '?'
142+
const customName = this.name?.trim() && this.name !== t('spreed', 'Guest') ? this.name : '?'
138143
return customName.charAt(0)
139144
},
140145
menuContainerWithFallback() {
@@ -166,7 +171,7 @@ export default {
166171
&--condensed {
167172
width: unset;
168173
height: unset;
169-
margin-left: -2px;
174+
margin-left: calc(var(--condensed-overlap) * -1px);
170175
display: flex;
171176
172177
& > .icon,

src/components/ChatView.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
role="region"
4747
:token="token"
4848
:container="containerId"
49+
has-typing-indicator
4950
:aria-label="t('spreed', 'Post message')" />
5051
</div>
5152
</template>

src/components/NewMessageForm/NewMessageForm.vue

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
-->
2222

2323
<template>
24-
<div class="wrapper">
24+
<div class="wrapper" :class="{'wrapper--has-typing-indicator': showTypingStatus}">
25+
<NewMessageFormTypingIndicator v-if="showTypingStatus"
26+
:token="token" />
27+
2528
<!--native file picker, hidden -->
2629
<input id="file-upload"
2730
ref="fileUploadInput"
@@ -245,11 +248,12 @@ import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
245248
246249
import Quote from '../Quote.vue'
247250
import AudioRecorder from './AudioRecorder/AudioRecorder.vue'
251+
import NewMessageFormTypingIndicator from './NewMessageFormTypingIndicator.vue'
248252
import SimplePollsEditor from './SimplePollsEditor/SimplePollsEditor.vue'
249253
import TemplatePreview from './TemplatePreview.vue'
250254
251255
import { useViewer } from '../../composables/useViewer.js'
252-
import { CONVERSATION, PARTICIPANT } from '../../constants.js'
256+
import { CONVERSATION, PARTICIPANT, PRIVACY } from '../../constants.js'
253257
import { EventBus } from '../../services/EventBus.js'
254258
import { shareFile, createTextFile } from '../../services/filesSharingServices.js'
255259
import { searchPossibleMentions } from '../../services/mentionsService.js'
@@ -267,31 +271,34 @@ const margin = 8
267271
const width = margin * 20
268272
269273
const disableKeyboardShortcuts = OCP.Accessibility.disableKeyboardShortcuts()
274+
const supportTypingStatus = getCapabilities()?.spreed?.config?.chat?.['typing-privacy'] !== undefined
270275
271276
export default {
272277
name: 'NewMessageForm',
273278
274279
disableKeyboardShortcuts,
275280
276281
components: {
277-
Quote,
278-
NcActions,
282+
AudioRecorder,
279283
NcActionButton,
284+
NcActions,
280285
NcButton,
281-
Paperclip,
282286
NcEmojiPicker,
287+
NcModal,
283288
NcRichContenteditable,
284-
EmoticonOutline,
285-
Send,
286-
AudioRecorder,
287-
BellOff,
289+
NcTextField,
290+
NewMessageFormTypingIndicator,
291+
Quote,
288292
SimplePollsEditor,
289-
Poll,
290-
NcModal,
293+
TemplatePreview,
294+
// Icons
295+
BellOff,
296+
EmoticonOutline,
291297
Folder,
298+
Paperclip,
299+
Poll,
300+
Send,
292301
Upload,
293-
TemplatePreview,
294-
NcTextField,
295302
},
296303
297304
props: {
@@ -328,6 +335,14 @@ export default {
328335
type: Boolean,
329336
default: false,
330337
},
338+
339+
/**
340+
* Show an indicator if someone is currently typing a message.
341+
*/
342+
hasTypingIndicator: {
343+
type: Boolean,
344+
default: false,
345+
},
331346
},
332347
333348
emits: ['sent', 'failure'],
@@ -336,7 +351,10 @@ export default {
336351
337352
setup() {
338353
const { openViewer } = useViewer()
339-
return { openViewer }
354+
return {
355+
openViewer,
356+
supportTypingStatus,
357+
}
340358
},
341359
342360
data() {
@@ -354,6 +372,7 @@ export default {
354372
checked: -1,
355373
userData: {},
356374
clipboardTimeStamp: null,
375+
typingTimeout: null,
357376
}
358377
},
359378
@@ -483,6 +502,10 @@ export default {
483502
showAudioRecorder() {
484503
return !this.hasText && this.canUploadFiles && !this.broadcast
485504
},
505+
showTypingStatus() {
506+
return this.hasTypingIndicator && this.supportTypingStatus
507+
&& this.$store.getters.getTypingStatusPrivacy() === PRIVACY.PUBLIC
508+
},
486509
},
487510
488511
watch: {
@@ -492,6 +515,21 @@ export default {
492515
493516
text(newValue) {
494517
this.$store.dispatch('setCurrentMessageInput', { token: this.token, text: newValue })
518+
519+
// Enable signal sending, only if indicator for this input is on
520+
if (this.showTypingStatus) {
521+
clearTimeout(this.typingTimeout)
522+
523+
if (!newValue) {
524+
this.$store.dispatch('setTyping', { typing: false })
525+
return
526+
}
527+
528+
this.typingTimeout = setTimeout(() => {
529+
this.$store.dispatch('setTyping', { typing: false })
530+
}, 5000)
531+
this.$store.dispatch('setTyping', { typing: true })
532+
}
495533
},
496534
497535
token(token) {
@@ -500,6 +538,7 @@ export default {
500538
} else {
501539
this.text = ''
502540
}
541+
this.$store.dispatch('setTyping', { typing: false })
503542
},
504543
505544
showTextFileDialog(newValue) {
@@ -929,22 +968,29 @@ export default {
929968
@import '../../assets/variables';
930969
931970
.wrapper {
971+
position: relative;
932972
display: flex;
933973
justify-content: center;
934-
padding: 12px 0;
974+
padding: 12px 0 12px;
935975
min-height: 69px;
976+
977+
&--has-typing-indicator {
978+
padding: 30px 0 12px;
979+
}
936980
}
937981
938982
.new-message {
939983
width: 100%;
940984
display: flex;
941985
justify-content: center;
986+
942987
&-form {
943988
align-items: flex-end;
944989
display: flex;
945-
position:relative;
990+
position: relative;
946991
flex: 0 1 700px;
947992
margin: 0 4px;
993+
948994
&__emoji-picker {
949995
position: absolute;
950996
bottom: 1px;
@@ -963,6 +1009,7 @@ export default {
9631009
border-radius: calc(var(--default-clickable-area) / 2);
9641010
padding: 8px 16px 8px 44px;
9651011
max-height: 180px;
1012+
9661013
&:hover,
9671014
&:focus,
9681015
&:active {
@@ -994,10 +1041,12 @@ export default {
9941041
// Targeting two classess for specificity
9951042
:deep(.action-item__menutoggle.action-item__menutoggle--with-icon-slot) {
9961043
opacity: 1 !important;
1044+
9971045
&:hover,
9981046
&:focus {
9991047
background-color: var(--color-background-hover) !important;
10001048
}
1049+
10011050
&:disabled {
10021051
opacity: .5 !important;
10031052
}

0 commit comments

Comments
 (0)