Show remaining character count in SMS app
## Summary This adds character counter below the “Send” button in SMS conversation. It uses format XXX/Y where XXX is number of characters that can be added without splitting the SMS into multiple messages (see article Concatenated SMS on Wikipedia). Y is number of messages in in current concatenated SMS. The counter is not visible when insertion of 10 7-bit or 16-bit (depends on SMS encoding) does not create concatenated SMS. SMS encoding is automatically guessed. 8-bit encodings are not supported. If the message contains characters that are not supported by GSM 7-bit encoding, counter automatically switches to UCS-2. ## Test Plan Try entering some text that is longer than 150 characters in [GSM 03.38 encoding][1] or 60 characters in UCS-2. Number of remaining characters should be visible below the “Send” button. The character counter should show `0` at exactly 160 or 70 characters. Inserting one character should switch the counter to [Concatenated SMS][2] mode when number of messages is shown. It should show exactly same number as SMS app in Android. ## Screenshots These images are in APNG. ![grab.apng](/uploads/21ae23f2fa75c7aca487e61ddce94644/grab.apng) ![grab-cz.apng](/uploads/785e670a8598c5a65a4209f17e75f578/grab-cz.apng) [1]: https://en.wikipedia.org/w/index.php?oldid=932080074#GSM_7-bit_default_alphabet_and_extension_table_of_3GPP_TS_23.038_/_GSM_03.38 [2]: https://en.wikipedia.org/w/index.php?oldid=943185255#Message_size
This commit is contained in:
parent
f211d7e401
commit
365791dc59
9 changed files with 443 additions and 34 deletions
|
@ -4,6 +4,7 @@ find_package(KF5People)
|
|||
|
||||
add_library(kdeconnectsmshelper
|
||||
smshelper.cpp
|
||||
gsmasciimap.cpp
|
||||
)
|
||||
|
||||
set_target_properties(kdeconnectsmshelper PROPERTIES
|
||||
|
|
|
@ -192,3 +192,21 @@ void ConversationModel::handleConversationLoaded(qint64 threadID, quint64 numMes
|
|||
// so we should not be showing a loading indicator
|
||||
Q_EMIT loadingFinished();
|
||||
}
|
||||
|
||||
QString ConversationModel::getCharCountInfo(const QString& message) const
|
||||
{
|
||||
SmsCharCount count = SmsHelper::getCharCount(message);
|
||||
|
||||
if (count.messages > 1) {
|
||||
// Show remaining char count and message count
|
||||
return QString::number(count.remaining) + QLatin1Char('/') + QString::number(count.messages);
|
||||
}
|
||||
if (count.messages == 1 && count.remaining < 10) {
|
||||
// Show only remaining char count
|
||||
return QString::number(count.remaining);
|
||||
}
|
||||
else {
|
||||
// Do not show anything
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,6 +78,8 @@ public:
|
|||
* TODO: Move to smshelper (or maybe make part of kirigami-addons chat library?)
|
||||
*/
|
||||
Q_INVOKABLE void copyToClipboard(const QString& message) const;
|
||||
|
||||
Q_INVOKABLE QString getCharCountInfo(const QString& message) const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void loadingFinished();
|
||||
|
|
154
smsapp/gsmasciimap.cpp
Normal file
154
smsapp/gsmasciimap.cpp
Normal file
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Copyright (C) 2020 Jiří Wolker <woljiri@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of
|
||||
* the License or (at your option) version 3 or any later version
|
||||
* accepted by the membership of KDE e.V. (or its successor approved
|
||||
* by the membership of KDE e.V.), which shall act as a proxy
|
||||
* defined in Section 14 of version 3 of the license.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Map used to determine that ASCII character is in GSM 03.38 7-bit alphabet.
|
||||
*
|
||||
* Only allowed control characters are CR and LF but GSM alphabet has more of them.
|
||||
*/
|
||||
bool gsm_ascii_map[] = {
|
||||
false, // 0x0, some control char
|
||||
false, // 0x1, some control char
|
||||
false, // 0x2, some control char
|
||||
false, // 0x3, some control char
|
||||
false, // 0x4, some control char
|
||||
false, // 0x5, some control char
|
||||
false, // 0x6, some control char
|
||||
false, // 0x7, some control char
|
||||
false, // 0x8, some control char
|
||||
false, // 0x9, some control char
|
||||
true, // 0xA, LF
|
||||
false, // 0xB, some control char
|
||||
false, // 0xC, some control char
|
||||
true, // 0xD, CR
|
||||
false, // 0xE, some control char
|
||||
false, // 0xF, some control char
|
||||
false, // 0x10, some control char
|
||||
false, // 0x11, some control char
|
||||
false, // 0x12, some control char
|
||||
false, // 0x13, some control char
|
||||
false, // 0x14, some control char
|
||||
false, // 0x15, some control char
|
||||
false, // 0x16, some control char
|
||||
false, // 0x17, some control char
|
||||
false, // 0x18, some control char
|
||||
false, // 0x19, some control char
|
||||
false, // 0x1A, some control char
|
||||
false, // 0x1B, some control char
|
||||
false, // 0x1C, some control char
|
||||
false, // 0x1D, some control char
|
||||
false, // 0x1E, some control char
|
||||
false, // 0x1F, some control char
|
||||
true, // 20, space
|
||||
true, // 21, !
|
||||
true, // 22, "
|
||||
true, // 23, #
|
||||
true, // 24, $
|
||||
true, // 25, %
|
||||
true, // 26, &
|
||||
true, // 27, '
|
||||
true, // 28, (
|
||||
true, // 29, )
|
||||
true, // 2A, *
|
||||
true, // 2B, +
|
||||
true, // 2C, ,
|
||||
true, // 2D, -
|
||||
true, // 2E, .
|
||||
true, // 2F, /
|
||||
true, // 30, 0
|
||||
true, // 31, 1
|
||||
true, // 32, 2
|
||||
true, // 33, 3
|
||||
true, // 34, 4
|
||||
true, // 35, 5
|
||||
true, // 36, 6
|
||||
true, // 37, 7
|
||||
true, // 38, 8
|
||||
true, // 39, 9
|
||||
true, // 3A, :
|
||||
true, // 3B, ;
|
||||
true, // 3C, <
|
||||
true, // 3D, =
|
||||
true, // 3E, >
|
||||
true, // 3F, ?
|
||||
true, // 40, @
|
||||
true, // 41, A
|
||||
true, // 42, B
|
||||
true, // 43, C
|
||||
true, // 44, D
|
||||
true, // 45, E
|
||||
true, // 46, F
|
||||
true, // 47, G
|
||||
true, // 48, H
|
||||
true, // 49, I
|
||||
true, // 4A, J
|
||||
true, // 4B, K
|
||||
true, // 4C, L
|
||||
true, // 4D, M
|
||||
true, // 4E, N
|
||||
true, // 4F, O
|
||||
true, // 50, P
|
||||
true, // 51, Q
|
||||
true, // 52, R
|
||||
true, // 53, S
|
||||
true, // 54, T
|
||||
true, // 55, U
|
||||
true, // 56, V
|
||||
true, // 57, W
|
||||
true, // 58, X
|
||||
true, // 59, Y
|
||||
true, // 5A, Z
|
||||
false, // 5B, [
|
||||
false, // 5C, backslash
|
||||
false, // 5D, ]
|
||||
false, // 5E, ^
|
||||
true, // 5F, _
|
||||
false, // 60, `
|
||||
true, // 61, a
|
||||
true, // 62, b
|
||||
true, // 63, c
|
||||
true, // 64, d
|
||||
true, // 65, e
|
||||
true, // 66, f
|
||||
true, // 67, g
|
||||
true, // 68, h
|
||||
true, // 69, i
|
||||
true, // 6A, j
|
||||
true, // 6B, k
|
||||
true, // 6C, l
|
||||
true, // 6D, m
|
||||
true, // 6E, n
|
||||
true, // 6F, o
|
||||
true, // 70, p
|
||||
true, // 71, q
|
||||
true, // 72, r
|
||||
true, // 73, s
|
||||
true, // 74, t
|
||||
true, // 75, u
|
||||
true, // 76, v
|
||||
true, // 77, w
|
||||
true, // 78, x
|
||||
true, // 79, y
|
||||
true, // 7A, z
|
||||
false, // 7B, {
|
||||
false, // 7C, |
|
||||
false, // 7D, }
|
||||
false, // 7E, ~
|
||||
};
|
27
smsapp/gsmasciimap.h
Normal file
27
smsapp/gsmasciimap.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Copyright (C) 2020 Jiří Wolker <woljiri@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of
|
||||
* the License or (at your option) version 3 or any later version
|
||||
* accepted by the membership of KDE e.V. (or its successor approved
|
||||
* by the membership of KDE e.V.), which shall act as a proxy
|
||||
* defined in Section 14 of version 3 of the license.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef GSMASCIIMAP_H
|
||||
#define GSMASCIIMAP_H
|
||||
|
||||
extern bool gsm_ascii_map[128];
|
||||
|
||||
#endif // GSMASCIIMAP_H
|
||||
|
|
@ -219,41 +219,50 @@ Kirigami.ScrollablePage
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
ToolButton {
|
||||
id: sendButton
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
padding: 0
|
||||
Kirigami.Icon {
|
||||
source: "document-send"
|
||||
enabled: sendButton.enabled
|
||||
isMask: true
|
||||
smooth: true
|
||||
anchors.centerIn: parent
|
||||
width: Kirigami.Units.gridUnit * 1.5
|
||||
height: width
|
||||
}
|
||||
onClicked: {
|
||||
// don't send empty messages
|
||||
if (!messageField.text.length) {
|
||||
return
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: sendButton
|
||||
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
|
||||
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
|
||||
padding: 0
|
||||
Kirigami.Icon {
|
||||
source: "document-send"
|
||||
enabled: sendButton.enabled
|
||||
isMask: true
|
||||
smooth: true
|
||||
anchors.centerIn: parent
|
||||
width: Kirigami.Units.gridUnit * 1.5
|
||||
height: width
|
||||
// disable the button to prevent sending
|
||||
// the same message several times
|
||||
sendButton.enabled = false
|
||||
|
||||
// send the message
|
||||
if (page.conversationId == page.invalidId) {
|
||||
conversationModel.sendMessageWithoutConversation(messageField.text, page.otherParty)
|
||||
} else {
|
||||
conversationModel.sendReplyToConversation(messageField.text)
|
||||
}
|
||||
messageField.text = ""
|
||||
|
||||
// re-enable the button
|
||||
sendButton.enabled = true
|
||||
}
|
||||
}
|
||||
onClicked: {
|
||||
// don't send empty messages
|
||||
if (!messageField.text.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// disable the button to prevent sending
|
||||
// the same message several times
|
||||
sendButton.enabled = false
|
||||
|
||||
// send the message
|
||||
if (page.conversationId == page.invalidId) {
|
||||
conversationModel.sendMessageWithoutConversation(messageField.text, page.otherParty)
|
||||
} else {
|
||||
conversationModel.sendReplyToConversation(messageField.text)
|
||||
}
|
||||
messageField.text = ""
|
||||
|
||||
// re-enable the button
|
||||
sendButton.enabled = true
|
||||
|
||||
|
||||
Label {
|
||||
id: "charCount"
|
||||
text: conversationModel.getCharCountInfo(messageField.text)
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
50
smsapp/smscharcount.h
Normal file
50
smsapp/smscharcount.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Copyright (C) 2020 Jiří Wolker <woljiri@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of
|
||||
* the License or (at your option) version 3 or any later version
|
||||
* accepted by the membership of KDE e.V. (or its successor approved
|
||||
* by the membership of KDE e.V.), which shall act as a proxy
|
||||
* defined in Section 14 of version 3 of the license.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef CHARCOUNT_H
|
||||
#define CHARCOUNT_H
|
||||
|
||||
class SmsCharCount {
|
||||
public:
|
||||
/**
|
||||
* Number of octets in current message.
|
||||
*/
|
||||
qint32 octets;
|
||||
|
||||
/**
|
||||
* Bits per character (7, 8 or 16).
|
||||
*/
|
||||
qint32 bitsPerChar;
|
||||
|
||||
/**
|
||||
* Number of chars remaining in current SMS.
|
||||
*/
|
||||
qint32 remaining;
|
||||
|
||||
/**
|
||||
* Count of SMSes in concatenated SMS.
|
||||
*/
|
||||
qint32 messages;
|
||||
|
||||
SmsCharCount(){};
|
||||
~SmsCharCount(){};
|
||||
};
|
||||
|
||||
#endif // CHARCOUNT_H
|
|
@ -32,6 +32,7 @@
|
|||
#include <KPeople/PersonsModel>
|
||||
|
||||
#include "interfaces/conversationmessage.h"
|
||||
#include "smsapp/gsmasciimap.h"
|
||||
|
||||
Q_LOGGING_CATEGORY(KDECONNECT_SMS_SMSHELPER, "kdeconnect.sms.smshelper")
|
||||
|
||||
|
@ -274,3 +275,139 @@ QIcon SmsHelper::getIconForAddresses(const QList<ConversationAddress>& addresses
|
|||
// around randomly (based on how the data came to us from Android)
|
||||
return combineIcons(icons);
|
||||
}
|
||||
|
||||
SmsCharCount SmsHelper::getCharCount(const QString& message)
|
||||
{
|
||||
const int remainingWhenEmpty = 160;
|
||||
const int septetsInSingleSms = 160;
|
||||
const int septetsInSingleConcatSms = 153;
|
||||
const int charsInSingleUcs2Sms = 70;
|
||||
const int charsInSingleConcatUcs2Sms = 67;
|
||||
|
||||
SmsCharCount count;
|
||||
bool enc7bit = true; // 7-bit is used when true, UCS-2 if false
|
||||
quint32 septets = 0; // GSM encoding character count (characters in extension are counted as 2 chars)
|
||||
int length = message.length();
|
||||
|
||||
// Count characters and detect encoding
|
||||
for (int i = 0; i < length; i++) {
|
||||
QChar ch = message[i];
|
||||
|
||||
if (isInGsmAlphabet(ch)) {
|
||||
septets++;
|
||||
}
|
||||
else if (isInGsmAlphabetExtension(ch)) {
|
||||
septets += 2;
|
||||
}
|
||||
else {
|
||||
enc7bit = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
count.bitsPerChar = 7;
|
||||
count.octets = 0;
|
||||
count.remaining = remainingWhenEmpty;
|
||||
count.messages = 1;
|
||||
}
|
||||
else if (enc7bit) {
|
||||
count.bitsPerChar = 7;
|
||||
count.octets = (septets * 7 + 6) / 8;
|
||||
if (septets > septetsInSingleSms) {
|
||||
count.messages = (septetsInSingleConcatSms - 1 + septets) / septetsInSingleConcatSms;
|
||||
count.remaining = (septetsInSingleConcatSms * count.messages - septets) % septetsInSingleConcatSms;
|
||||
}
|
||||
else {
|
||||
count.messages = 1;
|
||||
count.remaining = (septetsInSingleSms - septets) % septetsInSingleSms;
|
||||
}
|
||||
}
|
||||
else {
|
||||
count.bitsPerChar = 16;
|
||||
count.octets = length * 2; // QString should be in UTF-16
|
||||
if (length > charsInSingleUcs2Sms) {
|
||||
count.messages = (charsInSingleConcatUcs2Sms - 1 + length) / charsInSingleConcatUcs2Sms;
|
||||
count.remaining = (charsInSingleConcatUcs2Sms * count.messages - length) % charsInSingleConcatUcs2Sms;
|
||||
}
|
||||
else {
|
||||
count.messages = 1;
|
||||
count.remaining = (charsInSingleUcs2Sms - length) % charsInSingleUcs2Sms;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
bool SmsHelper::isInGsmAlphabet(const QChar& ch)
|
||||
{
|
||||
wchar_t unicode = ch.unicode();
|
||||
|
||||
if ((unicode & ~0x7f) == 0) { // If the character is ASCII
|
||||
// use map
|
||||
return gsm_ascii_map[unicode];
|
||||
}
|
||||
else {
|
||||
switch (unicode) {
|
||||
case 0xa1: // “¡”
|
||||
case 0xa7: // “§”
|
||||
case 0xbf: // “¿”
|
||||
case 0xa4: // “¤”
|
||||
case 0xa3: // “£”
|
||||
case 0xa5: // “¥”
|
||||
case 0xe0: // “à”
|
||||
case 0xe5: // “å”
|
||||
case 0xc5: // “Å”
|
||||
case 0xe4: // “ä”
|
||||
case 0xc4: // “Ä”
|
||||
case 0xe6: // “æ”
|
||||
case 0xc6: // “Æ”
|
||||
case 0xc7: // “Ç”
|
||||
case 0xe9: // “é”
|
||||
case 0xc9: // “É”
|
||||
case 0xe8: // “è”
|
||||
case 0xec: // “ì”
|
||||
case 0xf1: // “ñ”
|
||||
case 0xd1: // “Ñ”
|
||||
case 0xf2: // “ò”
|
||||
case 0xf5: // “ö”
|
||||
case 0xd6: // “Ö”
|
||||
case 0xf8: // “ø”
|
||||
case 0xd8: // “Ø”
|
||||
case 0xdf: // “ß”
|
||||
case 0xf9: // “ù”
|
||||
case 0xfc: // “ü”
|
||||
case 0xdc: // “Ü”
|
||||
case 0x393: // “Γ”
|
||||
case 0x394: // “Δ”
|
||||
case 0x398: // “Θ”
|
||||
case 0x39b: // “Λ”
|
||||
case 0x39e: // “Ξ”
|
||||
case 0x3a0: // “Π”
|
||||
case 0x3a3: // “Σ”
|
||||
case 0x3a6: // “Φ”
|
||||
case 0x3a8: // “Ψ”
|
||||
case 0x3a9: // “Ω”
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SmsHelper::isInGsmAlphabetExtension(const QChar& ch)
|
||||
{
|
||||
wchar_t unicode = ch.unicode();
|
||||
switch (unicode) {
|
||||
case '{':
|
||||
case '}':
|
||||
case '|':
|
||||
case '\\':
|
||||
case '^':
|
||||
case '[':
|
||||
case ']':
|
||||
case '~':
|
||||
case 0x20ac: // Euro sign
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
#include "interfaces/conversationmessage.h"
|
||||
|
||||
#include "kdeconnectsms_export.h"
|
||||
#include "smsapp/smscharcount.h"
|
||||
|
||||
Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_SMS_SMSHELPER)
|
||||
|
||||
|
@ -106,7 +107,17 @@ public:
|
|||
* Get the data for all persons currently stored on device
|
||||
*/
|
||||
static QList<QSharedPointer<KPeople::PersonData>> getAllPersons();
|
||||
|
||||
|
||||
/**
|
||||
* Get SMS character count status of SMS. It contains number of remaining characters
|
||||
* in current SMS (automatically selects 7-bit, 8-bit or 16-bit mode), octet count and
|
||||
* number of messages in concatenated SMS.
|
||||
*/
|
||||
static SmsCharCount getCharCount(const QString& message);
|
||||
|
||||
static bool isInGsmAlphabet(const QChar& ch);
|
||||
static bool isInGsmAlphabetExtension(const QChar& ch);
|
||||
|
||||
private:
|
||||
SmsHelper(){};
|
||||
~SmsHelper(){};
|
||||
|
|
Loading…
Reference in a new issue