summaryrefslogtreecommitdiff
path: root/src/client/app/common/views/components/messaging.vue
blob: 2ddec299843738d93ff9ce1afb8b70c52d021f15 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
<template>
<div class="mk-messaging" :data-compact="compact">
	<div class="search" v-if="!compact" :style="{ top: headerTop + 'px' }">
		<div class="form">
			<label for="search-input">%fa:search%</label>
			<input v-model="q" type="search" @input="search" @keydown="onSearchKeydown" placeholder="%i18n:@search-user%"/>
		</div>
		<div class="result">
			<ol class="users" v-if="result.length > 0" ref="searchResult">
				<li v-for="(user, i) in result"
					@keydown.enter="navigate(user)"
					@keydown="onSearchResultKeydown(i)"
					@click="navigate(user)"
					tabindex="-1"
				>
					<mk-avatar class="avatar" :user="user"/>
					<span class="name">{{ user | userName }}</span>
					<span class="username">@{{ user | acct }}</span>
				</li>
			</ol>
		</div>
	</div>
	<div class="history" v-if="messages.length > 0">
		<template>
			<a v-for="message in messages"
				class="user"
				:href="`/i/messaging/${getAcct(isMe(message) ? message.recipient : message.user)}`"
				:data-is-me="isMe(message)"
				:data-is-read="message.isRead"
				@click.prevent="navigate(isMe(message) ? message.recipient : message.user)"
				:key="message.id"
			>
				<div>
					<mk-avatar class="avatar" :user="isMe(message) ? message.recipient : message.user"/>
					<header>
						<span class="name">{{ isMe(message) ? message.recipient : message.user | userName }}</span>
						<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
						<mk-time :time="message.createdAt"/>
					</header>
					<div class="body">
						<p class="text"><span class="me" v-if="isMe(message)">%i18n:@you%:</span>{{ message.text }}</p>
					</div>
				</div>
			</a>
		</template>
	</div>
	<p class="no-history" v-if="!fetching && messages.length == 0">%i18n:@no-history%</p>
	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
</div>
</template>

<script lang="ts">
import Vue from 'vue';
import getAcct from '../../../../../acct/render';

export default Vue.extend({
	props: {
		compact: {
			type: Boolean,
			default: false
		},
		headerTop: {
			type: Number,
			default: 0
		}
	},
	data() {
		return {
			fetching: true,
			moreFetching: false,
			messages: [],
			q: null,
			result: [],
			connection: null,
			connectionId: null
		};
	},
	mounted() {
		this.connection = (this as any).os.streams.messagingIndexStream.getConnection();
		this.connectionId = (this as any).os.streams.messagingIndexStream.use();

		this.connection.on('message', this.onMessage);
		this.connection.on('read', this.onRead);

		(this as any).api('messaging/history').then(messages => {
			this.messages = messages;
			this.fetching = false;
		});
	},
	beforeDestroy() {
		this.connection.off('message', this.onMessage);
		this.connection.off('read', this.onRead);
		(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
	},
	methods: {
		getAcct,
		isMe(message) {
			return message.userId == this.$store.state.i.id;
		},
		onMessage(message) {
			this.messages = this.messages.filter(m => !(
				(m.recipientId == message.recipientId && m.userId == message.userId) ||
				(m.recipientId == message.userId && m.userId == message.recipientId)));

			this.messages.unshift(message);
		},
		onRead(ids) {
			ids.forEach(id => {
				const found = this.messages.find(m => m.id == id);
				if (found) found.isRead = true;
			});
		},
		search() {
			if (this.q == '') {
				this.result = [];
				return;
			}
			(this as any).api('users/search', {
				query: this.q,
				max: 5
			}).then(users => {
				this.result = users;
			});
		},
		navigate(user) {
			this.$emit('navigate', user);
		},
		onSearchKeydown(e) {
			switch (e.which) {
				case 9: // [TAB]
				case 40: // [↓]
					e.preventDefault();
					e.stopPropagation();
					(this.$refs.searchResult as any).childNodes[0].focus();
					break;
			}
		},
		onSearchResultKeydown(i, e) {
			const list = this.$refs.searchResult as any;

			const cancel = () => {
				e.preventDefault();
				e.stopPropagation();
			};

			switch (true) {
				case e.which == 27: // [ESC]
					cancel();
					(this.$refs.search as any).focus();
					break;

				case e.which == 9 && e.shiftKey: // [TAB] + [Shift]
				case e.which == 38: // [↑]
					cancel();
					(list.childNodes[i].previousElementSibling || list.childNodes[this.result.length - 1]).focus();
					break;

				case e.which == 9: // [TAB]
				case e.which == 40: // [↓]
					cancel();
					(list.childNodes[i].nextElementSibling || list.childNodes[0]).focus();
					break;
			}
		}
	}
});
</script>

<style lang="stylus" scoped>
@import '~const.styl'

root(isDark)

	&[data-compact]
		font-size 0.8em

		> .history
			> a
				&:last-child
					border-bottom none

				&:not([data-is-me]):not([data-is-read])
					> div
						background-image none
						border-left solid 4px #3aa2dc

				> div
					padding 16px

					> header
						> .mk-time
							font-size 1em

					> .avatar
						width 42px
						height 42px
						margin 0 12px 0 0

	> .search
		display block
		position -webkit-sticky
		position sticky
		top 0
		left 0
		z-index 1
		width 100%
		background #fff
		box-shadow 0 0px 2px rgba(#000, 0.2)

		> .form
			padding 8px
			background isDark ? #282c37 : #f7f7f7

			> label
				display block
				position absolute
				top 0
				left 8px
				z-index 1
				height 100%
				width 38px
				pointer-events none

				> [data-fa]
					display block
					position absolute
					top 0
					right 0
					bottom 0
					left 0
					width 1em
					line-height 56px
					margin auto
					color #555

			> input
				margin 0
				padding 0 0 0 32px
				width 100%
				font-size 1em
				line-height 38px
				color #000
				outline none
				background isDark ? #191b22 : #fff
				border solid 1px isDark ? #495156 : #eee
				border-radius 5px
				box-shadow none
				transition color 0.5s ease, border 0.5s ease

				&:hover
					border solid 1px isDark ? #b0b0b0 : #ddd
					transition border 0.2s ease

				&:focus
					color darken($theme-color, 20%)
					border solid 1px $theme-color
					transition color 0, border 0

		> .result
			display block
			top 0
			left 0
			z-index 2
			width 100%
			margin 0
			padding 0
			background #fff

			> .users
				margin 0
				padding 0
				list-style none

				> li
					display inline-block
					z-index 1
					width 100%
					padding 8px 32px
					vertical-align top
					white-space nowrap
					overflow hidden
					color rgba(#000, 0.8)
					text-decoration none
					transition none
					cursor pointer

					&:hover
					&:focus
						color #fff
						background $theme-color

						.name
							color #fff

						.username
							color #fff

					&:active
						color #fff
						background darken($theme-color, 10%)

						.name
							color #fff

						.username
							color #fff

					.avatar
						vertical-align middle
						min-width 32px
						min-height 32px
						max-width 32px
						max-height 32px
						margin 0 8px 0 0
						border-radius 6px

					.name
						margin 0 8px 0 0
						/*font-weight bold*/
						font-weight normal
						color rgba(#000, 0.8)

					.username
						font-weight normal
						color rgba(#000, 0.3)

	> .history

		> a
			display block
			text-decoration none
			background isDark ? #282c37 : #fff
			border-bottom solid 1px isDark ? #1c2023 : #eee

			*
				pointer-events none
				user-select none

			&:hover
				background isDark ? #1e2129 : #fafafa

				> .avatar
					filter saturate(200%)

			&:active
				background isDark ? #14161b : #eee

			&[data-is-read]
			&[data-is-me]
				opacity 0.8

			&:not([data-is-me]):not([data-is-read])
				> div
					background-image url("/assets/unread.svg")
					background-repeat no-repeat
					background-position 0 center

			&:after
				content ""
				display block
				clear both

			> div
				max-width 500px
				margin 0 auto
				padding 20px 30px

				&:after
					content ""
					display block
					clear both

				> header
					display flex
					align-items center
					margin-bottom 2px
					white-space nowrap
					overflow hidden

					> .name
						margin 0
						padding 0
						overflow hidden
						text-overflow ellipsis
						font-size 1em
						color isDark ? #fff : rgba(#000, 0.9)
						font-weight bold
						transition all 0.1s ease

					> .username
						margin 0 8px
						color isDark ? #606984 : rgba(#000, 0.5)

					> .mk-time
						margin 0 0 0 auto
						color isDark ? #606984 : rgba(#000, 0.5)
						font-size 80%

				> .avatar
					float left
					width 54px
					height 54px
					margin 0 16px 0 0
					border-radius 8px
					transition all 0.1s ease

				> .body

					> .text
						display block
						margin 0 0 0 0
						padding 0
						overflow hidden
						overflow-wrap break-word
						font-size 1.1em
						color isDark ? #fff : rgba(#000, 0.8)

						.me
							color isDark ? rgba(#fff, 0.7) : rgba(#000, 0.4)

					> .image
						display block
						max-width 100%
						max-height 512px

	> .no-history
		margin 0
		padding 2em 1em
		text-align center
		color #999
		font-weight 500

	> .fetching
		margin 0
		padding 16px
		text-align center
		color #aaa

		> [data-fa]
			margin-right 4px

	// TODO: element base media query
	@media (max-width 400px)
		> .search
			> .result
				> .users
					> li
						padding 8px 16px

		> .history
			> a
				&:not([data-is-me]):not([data-is-read])
					> div
						background-image none
						border-left solid 4px #3aa2dc

				> div
					padding 16px
					font-size 14px

					> .avatar
						margin 0 12px 0 0

.mk-messaging[data-darkmode]
	root(true)

.mk-messaging:not([data-darkmode])
	root(false)

</style>