Skip to content

Commit 82a8eb1

Browse files
committed
(feature) add user tags UI
1 parent 52c1f97 commit 82a8eb1

File tree

5 files changed

+185
-32
lines changed

5 files changed

+185
-32
lines changed

src/ChatWindow/ChatWindow.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ export default {
320320
border: var(--chat-container-border);
321321
border-radius: var(--chat-container-border-radius);
322322
box-shadow: var(--chat-container-box-shadow);
323+
-webkit-tap-highlight-color: transparent;
323324
324325
* {
325326
font-family: inherit;

src/ChatWindow/Room.vue

Lines changed: 164 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,11 @@
168168
v-show="Object.keys(room).length && showFooter"
169169
>
170170
<transition name="vac-slide-up">
171-
<div v-if="messageReply" class="vac-reply-container">
171+
<div
172+
v-if="messageReply"
173+
class="vac-reply-container"
174+
:style="{ bottom: `${roomFooterHeight}px` }"
175+
>
172176
<div class="vac-reply-box">
173177
<img
174178
v-if="isImageCheck(messageReply.file)"
@@ -191,7 +195,36 @@
191195
</div>
192196
</transition>
193197

194-
<div class="vac-box-footer">
198+
<transition name="vac-slide-up">
199+
<div
200+
v-if="filteredUsersTag.length"
201+
class="vac-tags-container vac-app-box-shadow"
202+
:style="{ bottom: `${roomFooterHeight}px` }"
203+
>
204+
<div
205+
class="vac-tags-box"
206+
v-for="user in filteredUsersTag"
207+
:key="user._id"
208+
@click="selectUserTag(user)"
209+
>
210+
<div class="vac-tags-info">
211+
<div
212+
v-if="user.avatar"
213+
class="vac-room-avatar vac-tags-avatar"
214+
:style="{ 'background-image': `url('${user.avatar}')` }"
215+
></div>
216+
<div class="vac-tags-username">
217+
{{ user.username }}
218+
</div>
219+
</div>
220+
</div>
221+
</div>
222+
</transition>
223+
224+
<div
225+
class="vac-box-footer"
226+
:class="{ 'vac-app-box-shadow': filteredUsersTag.length }"
227+
>
195228
<div class="vac-icon-textarea-left" v-if="showAudio && !imageFile">
196229
<div class="vac-svg-button" @click="recordAudio">
197230
<slot
@@ -255,7 +288,7 @@
255288
}"
256289
v-model="message"
257290
@input="onChangeInput"
258-
@keydown.esc="resetMessage"
291+
@keydown.esc="escapeTextarea"
259292
@keydown.enter.exact.prevent=""
260293
></textarea>
261294

@@ -339,6 +372,7 @@ import EmojiPicker from './EmojiPicker'
339372
340373
const { messagesValid } = require('../utils/roomValidation')
341374
const { detectMobile, iOSDevice } = require('../utils/mobileDetection')
375+
import filteredUsers from '../utils/filterItems'
342376
import typingText from '../utils/typingText'
343377
344378
export default {
@@ -402,33 +436,40 @@ export default {
402436
recorderStream: {},
403437
recorder: {},
404438
recordedChunks: [],
405-
keepKeyboardOpen: false
439+
keepKeyboardOpen: false,
440+
filteredUsersTag: [],
441+
textareaCursorPosition: null,
442+
roomFooterHeight: 0
406443
}
407444
},
408445
409446
mounted() {
410447
this.newMessages = []
448+
const isMobile = detectMobile()
411449
412450
window.addEventListener('keyup', e => {
413451
if (e.keyCode === 13 && !e.shiftKey) {
414-
if (detectMobile()) {
452+
if (isMobile) {
415453
this.message = this.message + '\n'
416454
setTimeout(() => this.onChangeInput(), 0)
417455
} else {
418456
this.sendMessage()
419457
}
420458
}
459+
460+
this.updateShowUsersTag()
421461
})
422462
423-
if (detectMobile()) {
424-
this.$refs['roomTextarea'].addEventListener('blur', () =>
425-
setTimeout(() => (this.keepKeyboardOpen = false), 0)
426-
)
427-
this.$refs['roomTextarea'].addEventListener(
428-
'click',
429-
() => (this.keepKeyboardOpen = true)
430-
)
431-
}
463+
this.$refs['roomTextarea'].addEventListener('click', () => {
464+
if (isMobile) this.keepKeyboardOpen = true
465+
this.updateShowUsersTag()
466+
})
467+
468+
this.$refs['roomTextarea'].addEventListener('blur', () => {
469+
this.filteredUsersTag = []
470+
this.textareaCursorPosition = null
471+
if (isMobile) setTimeout(() => (this.keepKeyboardOpen = false), 0)
472+
})
432473
433474
this.$refs.scrollContainer.addEventListener('scroll', e => {
434475
this.hideOptions = true
@@ -545,6 +586,55 @@ export default {
545586
},
546587
547588
methods: {
589+
updateShowUsersTag() {
590+
if (this.$refs['roomTextarea']) {
591+
if (
592+
this.textareaCursorPosition ===
593+
this.$refs['roomTextarea'].selectionStart
594+
) {
595+
return
596+
}
597+
598+
this.textareaCursorPosition = this.$refs['roomTextarea'].selectionStart
599+
600+
let n = this.textareaCursorPosition
601+
602+
while (
603+
n > 0 &&
604+
this.message.charAt(n - 1) !== '@' &&
605+
this.message.charAt(n - 1) !== ' '
606+
) {
607+
n--
608+
}
609+
610+
const beforeTag = this.message.charAt(n - 2)
611+
const notLetterNumber = !beforeTag.match(/^[0-9a-zA-Z]+$/)
612+
613+
if (
614+
this.message.charAt(n - 1) === '@' &&
615+
(!beforeTag || beforeTag === ' ' || notLetterNumber)
616+
) {
617+
const query = this.message.substring(n, this.textareaCursorPosition)
618+
619+
this.filteredUsersTag = filteredUsers(
620+
this.room.users,
621+
'username',
622+
query,
623+
true
624+
)
625+
} else {
626+
this.filteredUsersTag = []
627+
this.textareaCursorPosition = null
628+
}
629+
}
630+
},
631+
selectUserTag(user) {
632+
const cursorPosition = this.$refs['roomTextarea'].selectionStart - 1
633+
this.message =
634+
this.message.substr(0, cursorPosition + 1) +
635+
user.username +
636+
this.message.substr(cursorPosition + 1)
637+
},
548638
onImgLoad() {
549639
let height = this.$refs.imageFile.height
550640
if (height < 30) height = 30
@@ -624,6 +714,10 @@ export default {
624714
addNewMessage(message) {
625715
this.newMessages.push(message)
626716
},
717+
escapeTextarea() {
718+
if (this.filteredUsersTag.length) this.filteredUsersTag = []
719+
else this.resetMessage()
720+
},
627721
resetMessage(disableMobileFocus = null, editFile = null) {
628722
this.$emit('typing-message', null)
629723
@@ -755,6 +849,10 @@ export default {
755849
756850
el.style.height = 0
757851
el.style.height = el.scrollHeight - padding * 2 + 'px'
852+
853+
setTimeout(() => {
854+
this.roomFooterHeight = this.$refs['roomFooter'].clientHeight
855+
}, 10)
758856
},
759857
addEmoji(emoji) {
760858
this.message += emoji.icon
@@ -942,7 +1040,7 @@ export default {
9421040
}
9431041
9441042
.vac-room-footer {
945-
width: calc(100% - 1px);
1043+
width: 100%;
9461044
border-bottom-right-radius: 4px;
9471045
z-index: 10;
9481046
}
@@ -954,12 +1052,56 @@ export default {
9541052
padding: 10px 8px 10px;
9551053
}
9561054
1055+
.vac-tags-container {
1056+
position: absolute;
1057+
display: flex;
1058+
flex-direction: column;
1059+
align-items: center;
1060+
width: 100%;
1061+
1062+
.vac-tags-box {
1063+
display: flex;
1064+
width: 100%;
1065+
overflow: hidden;
1066+
cursor: pointer;
1067+
background: var(--chat-footer-bg-color);
1068+
1069+
&:hover {
1070+
background: var(--chat-footer-bg-color-tag-active);
1071+
transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
1072+
}
1073+
1074+
&:not(:hover) {
1075+
transition: background-color 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
1076+
}
1077+
}
1078+
1079+
.vac-tags-info {
1080+
display: flex;
1081+
overflow: hidden;
1082+
padding: 10px 20px;
1083+
align-items: center;
1084+
}
1085+
1086+
.vac-tags-avatar {
1087+
height: 34px;
1088+
width: 34px;
1089+
min-height: 34px;
1090+
min-width: 34px;
1091+
}
1092+
1093+
.vac-tags-username {
1094+
font-size: 14px;
1095+
}
1096+
}
1097+
9571098
.vac-reply-container {
1099+
position: absolute;
9581100
display: flex;
9591101
padding: 10px 10px 0 10px;
960-
background: var(--chat-content-bg-color);
1102+
background: var(--chat-footer-bg-color);
9611103
align-items: center;
962-
max-width: 100%;
1104+
width: calc(100% - 20px);
9631105
9641106
.vac-reply-box {
9651107
width: 100%;
@@ -1252,6 +1394,11 @@ export default {
12521394
12531395
.vac-reply-container {
12541396
padding: 5px 8px;
1397+
width: calc(100% - 16px);
1398+
}
1399+
1400+
.vac-tags-container .vac-tags-info {
1401+
padding: 8px 12px;
12551402
}
12561403
12571404
.vac-icon-scroll {

src/styles/helper.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
border-bottom: var(--chat-border-style);
1515
}
1616

17+
.vac-app-box-shadow {
18+
box-shadow: 0 2px 2px -4px rgba(0, 0, 0, 0.1),
19+
0 2px 2px 1px rgba(0, 0, 0, 0.12), 0 1px 8px 1px rgba(0, 0, 0, 0.12);
20+
}
21+
1722
.vac-item-clickable {
1823
cursor: pointer;
1924
}

src/themes/index.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export const defaultThemeStyles = {
2727
background: '#f8f9fa',
2828
borderStyleInput: '1px solid #e1e4e8',
2929
borderInputSelected: '#1976d2',
30-
backgroundReply: 'rgba(0, 0, 0, 0.08)'
30+
backgroundReply: '#e5e5e6',
31+
backgroundTagActive: '#e5e5e6'
3132
},
3233

3334
content: {
@@ -152,7 +153,8 @@ export const defaultThemeStyles = {
152153
background: '#131415',
153154
borderStyleInput: 'none',
154155
borderInputSelected: '#1976d2',
155-
backgroundReply: '#1b1c1c'
156+
backgroundReply: '#1b1c1c',
157+
backgroundTagActive: '#1b1c1c'
156158
},
157159

158160
content: {
@@ -290,6 +292,7 @@ export const cssThemeVars = ({
290292
'--chat-border-style-input': footer.borderStyleInput,
291293
'--chat-border-color-input-selected': footer.borderInputSelected,
292294
'--chat-footer-bg-color-reply': footer.backgroundReply,
295+
'--chat-footer-bg-color-tag-active': footer.backgroundTagActive,
293296

294297
// content
295298
'--chat-content-bg-color': content.background,

src/utils/filterItems.js

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
1-
export default (items, prop, val) => {
1+
export default (items, prop, val, startsWith = false) => {
22
if (!val || val === '') return items
33

44
return items.filter(v => {
5-
return (
6-
v[prop]
7-
.toLowerCase()
8-
.normalize('NFD')
9-
.replace(/[\u0300-\u036f]/g, '')
10-
.indexOf(
11-
val
12-
.toLowerCase()
13-
.normalize('NFD')
14-
.replace(/[\u0300-\u036f]/g, '')
15-
) > -1
16-
)
5+
if (startsWith) return formatString(v[prop]).startsWith(formatString(val))
6+
return formatString(v[prop]).includes(formatString(val))
177
})
188
}
9+
10+
function formatString(string) {
11+
return string
12+
.toLowerCase()
13+
.normalize('NFD')
14+
.replace(/[\u0300-\u036f]/g, '')
15+
}

0 commit comments

Comments
 (0)