Typing indicator support & increase SMS file limit

Fix issue with send btn & subject line
Fix read receipts being overridden
Show contact name in group message notification
This commit is contained in:
Aziz Hasanain 2021-02-27 16:27:59 +03:00
parent b330186cb3
commit c3cabb53fa
7 changed files with 201 additions and 43 deletions

View File

@ -2,7 +2,7 @@
"name": "webmessage",
"author": "sgtaziz",
"description": "A client for communicating with the WebMessage tweak on iOS. Send and receive messages from the comfort of your computer.",
"version": "0.5.0",
"version": "0.5.2",
"private": true,
"scripts": {
"serve": "vue-cli-service electron:serve",

View File

@ -345,7 +345,7 @@ export default {
if (this.$store.state.messagesCache[messageData.personId]) {
let oldMsgIndex = this.$store.state.messagesCache[messageData.personId].findIndex(obj => obj.guid == messageData.guid)
if (oldMsgIndex != -1) {
this.$store.state.messagesCache[messageData.personId][oldMsgIndex] = messageData
this.$set(this.$store.state.messagesCache[messageData.personId], oldMsgIndex, messageData)
return
}
this.$store.state.messagesCache[messageData.personId].unshift(messageData)
@ -353,10 +353,14 @@ export default {
if (messageData.sender != 1 && remote.Notification.isSupported()) {
if (this.$store.state.mutedChats.includes(messageData.personId)) return
let body = messageData.text.replace(/\u{fffc}/gu, "")
if (messageData.group && messageData.group.startsWith('chat')) {
body = `${messageData.author}: ${body}`
}
const notification = {
title: messageData.name,
body: messageData.text.replace(/\u{fffc}/gu, ""),
body: body,
silent: !this.$store.state.systemSound
}
@ -384,7 +388,7 @@ export default {
if (reactions && reactions.length > 0 && this.$store.state.messagesCache[reactions[0].personId]) {
let msgIndex = this.$store.state.messagesCache[reactions[0].personId].findIndex(obj => obj.guid == reactions[0].forGUID)
if (msgIndex > -1) {
this.$store.state.messagesCache[reactions[0].personId][msgIndex].reactions = reactions
this.$set(this.$store.state.messagesCache[reactions[0].personId][msgIndex], 'reactions', reactions)
}
}
@ -436,6 +440,14 @@ export default {
}
}
},
setTypingIndicator (data) {
if (data && data.chat_id) {
let chatId = data.chat_id
let typing = (data.typing == true)
if (chatId) this.$store.commit('setTyping', { chatId: chatId, isTyping: typing })
}
},
removeChat (data) {
if (data.chatId) {
let chatId = data.chatId

View File

@ -1,5 +1,5 @@
<template>
<div class="chatContainer" :class="this.$route.path == '/message/'+this.chatid ? 'active' : ''" :id="'id'+chatid" @click="navigate">
<div class="chatContainer" :class="$route.path == '/message/'+chatid ? 'active' : ''" :id="'id'+chatid" @click="navigate">
<div class="chatWrapper" @mousedown="dragMouseDown" :style="{ left: '-'+slideX+'px' }">
<div class="unread" :style="read ? 'background-color: transparent;' : ''"></div>
<div class="avatarContainer">
@ -10,12 +10,13 @@
<div class="title">
<span class="author">
<span class="name" v-html="$options.filters.twemoji(author)"></span>
<span v-if="showNum" class="number"> ({{ this.chatid }})</span>
<span v-if="showNum" class="number"> ({{ chatid }})</span>
<feather type="bell-off" stroke="rgb(85,85,85)" size="13" v-if="$store.state.mutedChats.includes(chatid)"></feather>
</span>
<span class="date">{{ date/1000 | moment }}</span>
</div>
<div class="text">
<typing-indicator v-if="$store.state.isTyping[chatid]"></typing-indicator>
<div class="text" v-else>
<span v-html="trimmedText"></span>
</div>
</div>
@ -29,8 +30,10 @@
<script>
import moment from 'moment'
import TypingIndicator from './TypingIndicator.vue'
export default {
components: { TypingIndicator },
name: 'Chat',
props: {
chatid: { type: String },

View File

@ -73,6 +73,12 @@
</template>
</div>
</div>
<div class="messageGroup receive last" v-if="$store.state.isTyping[$route.params.id]">
<div class="textWrapper">
<typing-indicator></typing-indicator>
</div>
</div>
</simplebar>
<div class="textboxContainer">
@ -85,7 +91,7 @@
</div>
</div>
<div class="msgTextboxWrapper">
<input type="text" v-if="$store.state.subjectLine" class="subjectLine" ref="subjectLine" placeholder="Subject" @keyup.enter.exact="sendText" :class="{ noTopBorder: hasAttachments }">
<input type="text" v-model="subjectInput" v-if="$store.state.subjectLine" class="subjectLine" ref="subjectLine" placeholder="Subject" @keyup.enter.exact="sendText" :class="{ noTopBorder: hasAttachments }">
<twemoji-textarea @contentChanged="autoResize" :placeholder="this.messages[0] ? this.messages[0].type.replace('SMS', 'Text Message') : 'Send a message'"
:emojiData="emojiDataAll"
:emojiGroups="emojiGroups"
@ -99,7 +105,7 @@
</div>
<img src="@/assets/loading.webp" style="height:26px;float:right;" v-if="!canSend" />
<upload-button v-show="canSend" ref="uploadButton" :enableiMessageAttachments="this.messages[0] && this.messages[0].type == 'iMessage'" @filesChanged="previewFiles" />
<div class="sendBtn" :class="{ cantSend: !canSend, SMS: this.messages[0] && this.messages[0].type == 'SMS' }" @click="sendText">
<div class="sendBtn" :class="{ cantSend: !canSend || !hasDataToSend, SMS: this.messages[0] && this.messages[0].type == 'SMS' }" @click="sendText">
<feather type="arrow-up" size="20"></feather>
</div>
</div>
@ -126,6 +132,7 @@ import ReactionMenu from './ReactionMenu'
import Reactions from './Reactions'
import axios from 'axios'
import { parseBuffer } from 'bplist-parser'
import TypingIndicator from './TypingIndicator.vue'
export default {
name: 'Message',
@ -138,12 +145,13 @@ export default {
DownloadAttachment,
UploadButton,
ReactionMenu,
Reactions
Reactions,
TypingIndicator
},
data: function () {
return {
messages: [],
messageText: [],
messageText: {},
limit: 25,
offset: 0,
receiver: '',
@ -157,7 +165,8 @@ export default {
reactingMessage: null,
reactingMessageGUID: null,
reactingMessageReactions: null,
reactingMessagePart: 0
reactingMessagePart: 0,
subjectInput: ''
}
},
computed: {
@ -173,10 +182,17 @@ export default {
})
return groups
},
hasDataToSend() {
let messageText = this.messageText[this.$route.params.id] ? this.messageText[this.$route.params.id] : ''
let subjectText = this.subjectInput
return messageText != '' || subjectText != '' || this.hasAttachments
},
sortedMessages() {
// let messages = this.messages.sort((a, b) => (b.date - a.date > 0 ? 1 : -1))
let messages = this.messages
let lastSentMessageFound = false
let lastReadMessageFound = false
const groupDates = (date1, date2) => (date2 - date1 < 3600000)
const groupAuthor = (author1, author2) => (author1 == author2)
@ -207,12 +223,40 @@ export default {
}
}
if (prev && groupAuthor(rest.author, prev.author) && groupAuthor(rest.sender, prev.sender) && groupDates(rest.date, prev.date))
r[r.length - 1].texts.unshift({ text: text, subject: subject, date: rest.date, attachments: rest.attachments, read: dateRead, delivered: dateDelivered, guid: guid, reactions: reactions, showStamp: rest.sender == 1 && !lastSentMessageFound, ...extras })
else
r.push({ ...rest, texts: [{ text: text, subject: subject, date: rest.date, attachments: rest.attachments, read: dateRead, delivered: dateDelivered, guid: guid, reactions: reactions, showStamp: rest.sender == 1 && !lastSentMessageFound, ...extras }] })
if (prev && groupAuthor(rest.author, prev.author) && groupAuthor(rest.sender, prev.sender) && groupDates(rest.date, prev.date)) {
r[r.length - 1].texts.unshift({
text: text,
subject: subject,
date: rest.date,
attachments: rest.attachments,
read: dateRead,
delivered: dateDelivered,
guid: guid,
reactions:
reactions,
showStamp: rest.sender == 1 && (!lastSentMessageFound || (!lastReadMessageFound && dateRead > 0)),
...extras
})
} else {
r.push({
...rest,
texts: [{
text: text,
subject: subject,
date: rest.date,
attachments: rest.attachments,
read: dateRead,
delivered: dateDelivered,
guid: guid,
reactions: reactions,
showStamp: rest.sender == 1 && (!lastSentMessageFound || (!lastReadMessageFound && dateRead > 0)),
...extras
}]
})
}
if (rest.sender == 1 && !lastSentMessageFound) lastSentMessageFound = true
if (rest.sender == 1 && (!lastReadMessageFound && dateRead > 0)) lastReadMessageFound = true
return r
}, [])
@ -397,12 +441,9 @@ export default {
autoResize (value) {
var el = document.getElementById('twemoji-textarea')
// if (!this.canSend) {
// el.innerHTML = this.messageText[this.$route.params.id]
// return
// }
if (value) this.messageText[this.$route.params.id] = value
if (value !== false) {
this.$set(this.messageText, this.$route.params.id, value)
}
this.$nextTick(() => {
let scrollHeight = el.scrollHeight
@ -432,7 +473,7 @@ export default {
sendText () {
if (!this.canSend) return
let messageText = this.messageText[this.$route.params.id]
let messageText = this.messageText[this.$route.params.id] ? this.messageText[this.$route.params.id] : ''
let subjectText = this.$refs.subjectLine ? this.$refs.subjectLine.value : ''
if (messageText == '' && subjectText != '') {
@ -461,17 +502,19 @@ export default {
}
this.canSend = true
this.autoResize('')
this.autoResize(false)
})
.catch(error => {
alert("There was an error while sending your text.\n" + error)
this.canSend = true
this.autoResize(false)
})
},
scrollToBottom () {
scrollToBottom (force) {
if (this.$refs.messages) {
let container = this.$refs.messages.SimpleBar.getScrollElement()
let scrollTo = this.lastHeight ? (container.scrollHeight - this.lastHeight) : container.scrollHeight
if (force && this.offset <= 25) scrollTo = container.scrollHeight
container.scrollTop = scrollTo
@ -510,8 +553,8 @@ export default {
}
},
previewFiles () {
this.hasAttachments = this.$refs.uploadButton.attachments != null
this.autoResize()
this.hasAttachments = this.$refs.uploadButton && this.$refs.uploadButton.attachments != null
this.autoResize(false)
},
removeAttachment (i) {
if (!this.canSend) return
@ -613,13 +656,20 @@ export default {
if (messageIndex > -1) {
this.messages[messageIndex]['dateRead'] = data.read
this.messages[messageIndex]['dateDelivered'] = data.delivered
this.$set(this.messages, messageIndex, this.messages[messageIndex]) // Reactivity in Vue is weird
if (this.lastHeight == null) {
this.$nextTick(() => { this.scrollToBottom() })
}
this.messages.__ob__.dep.notify()
}
}
},
setTypingIndicator (data) {
if (data && data.chat_id == this.$route.params.id && this.lastHeight == null) {
this.$nextTick(this.scrollToBottom)
setTimeout(this.scrollToBottom, 10)
}
},
newMessage (data) {
var message = data.message
@ -638,10 +688,11 @@ export default {
if (this.messages && this.messages.length > 0 && message[0]['personId'] == this.$route.params.id) {
let oldMsgIndex = this.messages.findIndex(obj => obj.guid == message[0].guid)
if (oldMsgIndex != -1) {
this.messages[oldMsgIndex] = message[0]
this.$set(this.messages, oldMsgIndex, message[0])
return
}
if (message[0].sender != 1) this.$store.commit('setTyping', { chatId: this.$route.params.id, isTyping: false })
this.messages.unshift(message[0])
if (this.lastHeight == null) {
@ -661,7 +712,8 @@ export default {
let msgIndex = this.messages.findIndex(obj => obj.guid == reactions[0].forGUID)
if (msgIndex > -1) {
this.messages[msgIndex].reactions = reactions
this.messages.__ob__.dep.notify()
this.$set(this.messages[msgIndex], 'reactions', reactions)
let intervalTime = 0
this.$nextTick(() => {
if (this.lastHeight == null) {
@ -794,6 +846,10 @@ export default {
font-weight: 500;
letter-spacing: 0.25px;
&::placeholder {
color: lighten(#7E7E7E, 25%);
}
&.noTopBorder {
border-top-left-radius: 0px;
border-top-right-radius: 0px;
@ -1130,7 +1186,8 @@ export default {
.attachment {
// max-width: 75%;
// max-height: 60vh;
width: 100%;
width: max-content;
max-width: 100%;
height: auto;
border-radius: 18px;
padding-bottom: 1px;
@ -1142,7 +1199,7 @@ export default {
// max-height: 700px;
border-radius: 18px;
border-radius: 12px;
max-height: 60vh;
max-height: 33vh;
max-width: 100%;
width: auto;
height: auto;
@ -1153,7 +1210,7 @@ export default {
// max-width: 420px;
// max-height: 700px;
border-radius: 12px;
max-height: 60vh;
max-height: 33vh;
max-width: 100%;
width: auto;
height: auto;
@ -1200,7 +1257,6 @@ export default {
align-items: flex-start;
flex-direction: column;
margin-right: 25%;
width: 75%;
max-width: 75%;
margin-bottom: 1px;
}
@ -1259,17 +1315,11 @@ export default {
.send {
align-items: flex-end;
.attachment {
display: inline-flex;
justify-content: flex-end;
}
.textWrapper {
display: flex;
align-items: flex-end;
flex-direction: column;
margin-left: 25%;
width: 75%;
max-width: 75%;
margin-bottom: 1px;
}

View File

@ -0,0 +1,82 @@
<template>
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</template>
<script>
export default {
name: "TypingIndicator",
props: {
},
mounted() {
},
methods: {
},
}
</script>
<style lang="scss" scoped>
.typing-indicator {
$ti-color-bg: #3A3A3C;
background-color: $ti-color-bg;
will-change: transform;
width: auto;
border-radius: 50px;
padding: 10px;
display: table;
margin: 0 auto;
position: relative;
animation: 2s bulge infinite ease-out;
margin-bottom: 5px;
margin-left: 0;
&::before,
&::after {
content: '';
position: absolute;
bottom: -2px;
left: -2px;
height: 10px;
width: 10px;
border-radius: 50%;
background-color: $ti-color-bg;
}
&::after {
height: 5px;
width: 5px;
left: -5px;
bottom: -5px;
}
span {
height: 7px;
width: 7px;
float: left;
margin: 0 1px;
background-color: #9E9EA1;
display: block;
border-radius: 50%;
opacity: 0.4;
@for $i from 1 through 3 {
&:nth-of-type(#{$i}) {
animation: 1s blink infinite ($i * .3333s);
}
}
}
}
@keyframes blink {
50% {
opacity: 1;
}
}
@keyframes bulge {
50% {
transform: scale(1.05);
}
}
</style>

View File

@ -41,8 +41,8 @@ export default {
if (!this.enableiMessageAttachments) {
// SMS limitations recommend a max payload size of 300kB.
// This can vary between carriers, but there is no way to
// detect it. So, recommended value it is.
this.sizeLimit = 0.3
// detect it. So, recommended value it is. But 20MB works so..
this.sizeLimit = 20
}
},
methods: {

View File

@ -25,7 +25,9 @@ export default new Vuex.Store({
enableTunnel: persistentStore.get('enableTunnel', false),
cacheMessages: persistentStore.get('cacheMessages', false),
mutedChats: persistentStore.get('mutedChats', []),
notifSound: persistentStore.get('notifSound', 'wm-audio://receivedText.mp3')
notifSound: persistentStore.get('notifSound', 'wm-audio://receivedText.mp3'),
isTyping: {},
isTypingTimer: {}
},
mutations: {
setPassword(state, password) {
@ -105,6 +107,15 @@ export default new Vuex.Store({
},
resetMessages(state) {
state['messagesCache'] = []
},
setTyping(state, data) {
Vue.set(state['isTyping'], data.chatId, data.isTyping)
if (state['isTypingTimer'][data.chatId]) clearTimeout(state['isTypingTimer'][data.chatId])
state['isTypingTimer'][data.chatId] = setTimeout(() => {
Vue.set(state['isTyping'], data.chatId, false)
}, 60000)
}
},
getters: {