Merge pull request #205 from Wunderfitz/new-chat-from-contacts-7

New chat from contacts, initial secret chat support
This commit is contained in:
Sebastian Wolf 2020-11-29 12:41:25 +01:00 committed by GitHub
commit 965f6db84e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 1949 additions and 69 deletions

View file

@ -24,6 +24,7 @@ SOURCES += src/harbour-fernschreiber.cpp \
src/appsettings.cpp \
src/chatlistmodel.cpp \
src/chatmodel.cpp \
src/contactsmodel.cpp \
src/dbusadaptor.cpp \
src/dbusinterface.cpp \
src/emojisearchworker.cpp \
@ -72,6 +73,7 @@ DISTFILES += qml/harbour-fernschreiber.qml \
qml/pages/ChatSelectionPage.qml \
qml/pages/CoverPage.qml \
qml/pages/InitializationPage.qml \
qml/pages/NewChatPage.qml \
qml/pages/OverviewPage.qml \
qml/pages/AboutPage.qml \
qml/pages/PollCreationPage.qml \
@ -145,6 +147,7 @@ HEADERS += \
src/appsettings.h \
src/chatlistmodel.h \
src/chatmodel.h \
src/contactsmodel.h \
src/dbusadaptor.h \
src/dbusinterface.h \
src/debuglog.h \

View file

@ -1,5 +1,6 @@
import QtQuick 2.6
import Sailfish.Silica 1.0
import WerkWolf.Fernschreiber 1.0
import "../js/twemoji.js" as Emoji
import "../js/functions.js" as Functions
@ -14,12 +15,13 @@ PhotoTextsListItem {
// chat title
primaryText.text: title ? Emoji.emojify(title + ( display.notification_settings.mute_for > 0 ? " 🔇" : "" ), Theme.fontSizeMedium) : qsTr("Unknown")
// last user
prologSecondaryText.text: is_channel ? "" : ( last_message_sender_id ? ( last_message_sender_id !== ownUserId ? Emoji.emojify(Functions.getUserName(tdLibWrapper.getUserInformation(last_message_sender_id)), primaryText.font.pixelSize) : qsTr("You") ) : qsTr("Unknown") )
prologSecondaryText.text: is_channel ? "" : ( last_message_sender_id ? ( last_message_sender_id !== ownUserId ? Emoji.emojify(Functions.getUserName(tdLibWrapper.getUserInformation(last_message_sender_id)), primaryText.font.pixelSize) : qsTr("You") ) : "" )
// last message
secondaryText.text: last_message_text ? Emoji.emojify(Functions.enhanceHtmlEntities(last_message_text), Theme.fontSizeExtraSmall) : qsTr("Unknown")
secondaryText.text: last_message_text ? Emoji.emojify(Functions.enhanceHtmlEntities(last_message_text), Theme.fontSizeExtraSmall) : "<i>" + qsTr("No message in this chat.") + "</i>"
// message date
tertiaryText.text: ( last_message_date ? Functions.getDateTimeElapsed(last_message_date) : qsTr("Unknown") ) + Emoji.emojify(last_message_status, tertiaryText.font.pixelSize)
tertiaryText.text: ( last_message_date ? ( last_message_date.length === 0 ? "" : Functions.getDateTimeElapsed(last_message_date) + Emoji.emojify(last_message_status, tertiaryText.font.pixelSize) ) : "" )
unreadCount: unread_count
isSecret: ( chat_type === TelegramAPI.ChatTypeSecret )
openMenuOnPressAndHold: true//chat_id != overviewPage.ownUserId
menu: ContextMenu {

View file

@ -1,5 +1,7 @@
import QtQuick 2.6
import Sailfish.Silica 1.0
import WerkWolf.Fernschreiber 1.0
ListItem {
id: chatListViewItem
@ -9,7 +11,8 @@ ListItem {
property alias secondaryText: secondaryText //usually last message
property alias tertiaryText: tertiaryText //usually last message date
property int unreadCount
property int unreadCount: 0
property bool isSecret: false
property alias pictureThumbnail: pictureThumbnail
contentHeight: mainRow.height + separator.height + 2 * Theme.paddingMedium
@ -50,6 +53,24 @@ ListItem {
height: parent.width
}
Rectangle {
id: chatSecretBackground
color: Theme.overlayBackgroundColor
width: Theme.fontSizeExtraLarge
height: Theme.fontSizeExtraLarge
anchors.bottom: parent.bottom
radius: parent.width / 2
visible: chatListViewItem.isSecret
}
Image {
source: "image://theme/icon-s-secure"
height: Theme.fontSizeMedium
width: Theme.fontSizeMedium
anchors.centerIn: chatSecretBackground
visible: chatListViewItem.isSecret
}
Rectangle {
id: chatUnreadMessagesCountBackground
color: Theme.highlightBackgroundColor
@ -104,6 +125,7 @@ ListItem {
width: parent.width - Theme.paddingMedium - prologSecondaryText.width
truncationMode: TruncationMode.Fade
textFormat: Text.StyledText
visible: prologSecondaryText.width < ( parent.width - Theme.paddingLarge )
}
}

View file

@ -41,6 +41,15 @@ SilicaFlickable {
tdLibWrapper.getUserFullInfo(chatInformationPage.chatPartnerGroupId);
tdLibWrapper.getUserProfilePhotos(chatInformationPage.chatPartnerGroupId, 100, 0);
break;
case "chatTypeSecret":
chatInformationPage.isSecretChat = true;
chatInformationPage.chatPartnerGroupId = chatInformationPage.chatInformation.type.user_id.toString();
if(!chatInformationPage.privateChatUserInformation.id) {
chatInformationPage.privateChatUserInformation = tdLibWrapper.getUserInformation(chatInformationPage.chatPartnerGroupId);
}
tdLibWrapper.getUserFullInfo(chatInformationPage.chatPartnerGroupId);
tdLibWrapper.getUserProfilePhotos(chatInformationPage.chatPartnerGroupId, 100, 0);
break;
case "chatTypeBasicGroup":
chatInformationPage.isBasicGroup = true;
chatInformationPage.chatPartnerGroupId = chatInformation.type.basic_group_id.toString();
@ -60,8 +69,8 @@ SilicaFlickable {
chatInformationPage.isChannel = chatInformationPage.groupInformation.is_channel;
break;
}
Debug.log("is set up", chatInformationPage.isPrivateChat, chatInformationPage.isBasicGroup, chatInformationPage.isSuperGroup, chatInformationPage.chatPartnerGroupId)
if(!chatInformationPage.isPrivateChat) {
Debug.log("is set up", chatInformationPage.isPrivateChat, chatInformationPage.isSecretChat, chatInformationPage.isBasicGroup, chatInformationPage.isSuperGroup, chatInformationPage.chatPartnerGroupId)
if(!(chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat)) {
updateGroupStatusText();
}
@ -147,18 +156,18 @@ SilicaFlickable {
}
}
onUserFullInfoReceived: {
if(chatInformationPage.isPrivateChat && userFullInfo["@extra"] === chatInformationPage.chatPartnerGroupId) {
if((chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) && userFullInfo["@extra"] === chatInformationPage.chatPartnerGroupId) {
chatInformationPage.chatPartnerFullInformation = userFullInfo;
}
}
onUserFullInfoUpdated: {
if(chatInformationPage.isPrivateChat && userId === chatInformationPage.chatPartnerGroupId) {
if((chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) && userId === chatInformationPage.chatPartnerGroupId) {
chatInformationPage.chatPartnerFullInformation = userFullInfo;
}
}
onUserProfilePhotosReceived: {
if(chatInformationPage.isPrivateChat && extra === chatInformationPage.chatPartnerGroupId) {
if((chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) && extra === chatInformationPage.chatPartnerGroupId) {
chatInformationPage.chatPartnerProfilePhotos = photos;
}
}
@ -180,14 +189,10 @@ SilicaFlickable {
}
}
Component.onCompleted: {7
Component.onCompleted: {
initializePage();
}
ListModel {
id: membersList
}
@ -227,13 +232,13 @@ SilicaFlickable {
}
text: chatInformation.notification_settings.mute_for > 0 ? qsTr("Unmute Chat") : qsTr("Mute Chat")
}
// MenuItem { //TODO Implement
// visible: !userIsMember
// onClicked: {
// tdLibWrapper.joinChat(chatInformationPage.chatInformation.id);
// }
// text: qsTr("Join Chat")
// }
MenuItem {
visible: chatInformationPage.isPrivateChat
onClicked: {
tdLibWrapper.createNewSecretChat(chatInformationPage.chatPartnerGroupId);
}
text: qsTr("New Secret Chat")
}
}
// header
PageHeader {
@ -278,14 +283,14 @@ SilicaFlickable {
active: imageContainer.hasImage
asynchronous: true
anchors.fill: chatPictureThumbnail
source: chatInformationPage.isPrivateChat
source: ( chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat)
? "ChatInformationProfilePictureList.qml"
: "ChatInformationProfilePicture.qml"
}
}
leftMargin: imageContainer.getEased((imageContainer.minDimension + Theme.paddingMedium), 0, imageContainer.tweenFactor) + Theme.horizontalPageMargin
title: chatInformationPage.chatInformation.title !== "" ? Emoji.emojify(chatInformationPage.chatInformation.title, Theme.fontSizeLarge) : qsTr("Unknown")
description: chatInformationPage.isPrivateChat ? ("@"+(chatInformationPage.privateChatUserInformation.username || chatInformationPage.chatPartnerGroupId)) : ""
description: (chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) ? ("@"+(chatInformationPage.privateChatUserInformation.username || chatInformationPage.chatPartnerGroupId)) : ""
}
SilicaFlickable {
@ -350,7 +355,7 @@ SilicaFlickable {
ChatInformationEditArea {
visible: canEdit
canEdit: !chatInformationPage.isPrivateChat && chatInformationPage.groupInformation.status && (chatInformationPage.groupInformation.status.can_change_info || chatInformationPage.groupInformation.status["@type"] === "chatMemberStatusCreator")
canEdit: !(chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) && chatInformationPage.groupInformation.status && (chatInformationPage.groupInformation.status.can_change_info || chatInformationPage.groupInformation.status["@type"] === "chatMemberStatusCreator")
headerText: qsTr("Chat Title", "group title header")
text: chatInformationPage.chatInformation.title
@ -375,13 +380,13 @@ SilicaFlickable {
}
}
ChatInformationEditArea {
canEdit: (chatInformationPage.isPrivateChat && chatInformationPage.privateChatUserInformation.id === chatInformationPage.myUserId) || ((chatInformationPage.isBasicGroup || chatInformationPage.isSuperGroup) && chatInformationPage.groupInformation && (chatInformationPage.groupInformation.status.can_change_info || chatInformationPage.groupInformation.status["@type"] === "chatMemberStatusCreator"))
canEdit: ((chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) && chatInformationPage.privateChatUserInformation.id === chatInformationPage.myUserId) || ((chatInformationPage.isBasicGroup || chatInformationPage.isSuperGroup) && chatInformationPage.groupInformation && (chatInformationPage.groupInformation.status.can_change_info || chatInformationPage.groupInformation.status["@type"] === "chatMemberStatusCreator"))
emptyPlaceholderText: qsTr("There is no information text available, yet.")
headerText: qsTr("Info", "group or user infotext header")
multiLine: true
text: (chatInformationPage.isPrivateChat ? chatInformationPage.chatPartnerFullInformation.bio : chatInformationPage.groupFullInformation.description) || ""
text: ((chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) ? chatInformationPage.chatPartnerFullInformation.bio : chatInformationPage.groupFullInformation.description) || ""
onSaveButtonClicked: {
if(chatInformationPage.isPrivateChat) { // own bio
if ((chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat)) { // own bio
tdLibWrapper.setBio(textValue);
} else { // group info
tdLibWrapper.setChatDescription(chatInformationPage.chatInformation.id, textValue);
@ -391,7 +396,7 @@ SilicaFlickable {
ChatInformationTextItem {
headerText: qsTr("Phone Number", "user phone number header")
text: (chatInformationPage.isPrivateChat && chatInformationPage.privateChatUserInformation.phone_number ? "+"+chatInformationPage.privateChatUserInformation.phone_number : "") || ""
text: ((chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) && chatInformationPage.privateChatUserInformation.phone_number ? "+"+chatInformationPage.privateChatUserInformation.phone_number : "") || ""
isLinkedLabel: true
}
@ -408,7 +413,7 @@ SilicaFlickable {
visible: !!inviteLinkItem.text
ChatInformationTextItem {
id: inviteLinkItem
text: !isPrivateChat ? chatInformationPage.groupFullInformation.invite_link : ""
text: !(chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) ? chatInformationPage.groupFullInformation.invite_link : ""
width: parent.width - inviteLinkButton.width
}
IconButton {

View file

@ -27,15 +27,15 @@ import "../../js/functions.js" as Functions
ChatInformationTabItemBase {
id: tabBase
loadingText: isPrivateChat ? qsTr("Loading common chats…", "chats you have in common with a user") : qsTr("Loading group members…")
loading: ( chatInformationPage.isSuperGroup || chatInformationPage.isPrivateChat) && !chatInformationPage.isChannel
loadingText: (isPrivateChat || isSecretChat) ? qsTr("Loading common chats…", "chats you have in common with a user") : qsTr("Loading group members…")
loading: ( chatInformationPage.isSuperGroup || chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) && !chatInformationPage.isChannel
loadingVisible: loading && membersView.count === 0
property var chatPartnerCommonGroupsIds: ([]);
SilicaListView {
id: membersView
model: chatInformationPage.isPrivateChat ? (chatPartnerCommonGroupsIds.length > 0 ? delegateModel : null) : pageContent.membersList
model: (chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) ? (chatPartnerCommonGroupsIds.length > 0 ? delegateModel : null) : pageContent.membersList
clip: true
height: tabBase.height
width: tabBase.width
@ -68,7 +68,7 @@ ChatInformationTabItemBase {
ViewPlaceholder {
y: Theme.paddingLarge
enabled: membersView.count === 0
text: chatInformationPage.isPrivateChat ? qsTr("You don't have any groups in common with this user.") : ( chatInformationPage.isChannel ? qsTr("Channel members are anonymous.") : qsTr("This group is empty.") )
text: (chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) ? qsTr("You don't have any groups in common with this user.") : ( chatInformationPage.isChannel ? qsTr("Channel members are anonymous.") : qsTr("This group is empty.") )
}
delegate: PhotoTextsListItem {
pictureThumbnail {
@ -204,7 +204,7 @@ ChatInformationTabItemBase {
}
}
onChatsReceived: {// common chats with user
if(isPrivateChat && chats["@extra"] === chatInformationPage.chatPartnerGroupId) {
if((isPrivateChat || isSecretChat) && chats["@extra"] === chatInformationPage.chatPartnerGroupId) {
tabBase.chatPartnerCommonGroupsIds = chats.chat_ids;
delegateModel.applyFilter();
// if we set it directly, the views start scrolling
@ -221,7 +221,7 @@ ChatInformationTabItemBase {
}
Component.onCompleted: {
if(chatInformationPage.isPrivateChat) {
if(chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat) {
tdLibWrapper.getGroupsInCommon(chatInformationPage.chatPartnerGroupId, 200, 0); // we only use the first 200
} else if(chatInformationPage.isSuperGroup) {
fetchMoreMembersTimer.start();

View file

@ -124,14 +124,14 @@ Item {
}
}
Component.onCompleted: {
if(!(isPrivateChat && chatPartnerGroupId === myUserId.toString())) {
if(!((isPrivateChat || isSecretChat) && chatPartnerGroupId === myUserId.toString())) {
tabModel.append({
tab:"ChatInformationTabItemMembersGroups",
title: chatInformationPage.isPrivateChat ? qsTr("Groups", "Button: groups in common (short)") : qsTr("Members", "Button: Group Members"),
title: ( chatInformationPage.isPrivateChat || chatInformationPage.isSecretChat ) ? qsTr("Groups", "Button: groups in common (short)") : qsTr("Members", "Button: Group Members"),
image: "image://theme/icon-m-people"
});
}
if(!isPrivateChat && (groupInformation.status.can_restrict_members || groupInformation.status["@type"] === "chatMemberStatusCreator")) {
if(!(isPrivateChat || isSecretChat) && (groupInformation.status.can_restrict_members || groupInformation.status["@type"] === "chatMemberStatusCreator")) {
tabModel.append({
tab:"ChatInformationTabItemSettings",
title: qsTr("Settings", "Button: Chat Settings"),

View file

@ -160,6 +160,7 @@ function getMessageText(message, simple, myself) {
return myself ? qsTr("sent an unsupported message: %1", "myself; %1 is message type").arg(message.content['@type'].substring(7)) : qsTr("sent an unsupported message: %1", "%1 is message type").arg(message.content['@type'].substring(7));
}
function getChatPartnerStatusText(statusType, was_online) {
switch(statusType) {
case "userStatusEmpty":
@ -176,6 +177,18 @@ function getChatPartnerStatusText(statusType, was_online) {
return qsTr("offline, was recently online");
}
}
function getSecretChatStatus(secretChatDetails) {
switch (secretChatDetails.state["@type"]) {
case "secretChatStateClosed":
return "<b>" + qsTr("Closed!") + "</b>";
case "secretChatStatePending":
return qsTr("Pending acknowledgement");
case "secretChatStateReady":
return "";
}
}
function getChatMemberStatusText(statusType) {
// chatMemberStatusAdministrator, chatMemberStatusBanned, chatMemberStatusCreator, chatMemberStatusLeft, chatMemberStatusMember, and chatMemberStatusRestricted.
switch(statusType) {

View file

@ -54,10 +54,10 @@ function emojify(str, emojiSize) {
basePath,
iconId,
'.svg',
'" align="middle" width="',
emojiSize,
'" align="bottom" width="',
Math.round(emojiSize * 6 / 5 ),
'" height="',
emojiSize,
Math.round(emojiSize * 6 / 5 ),
'"/>'
);
}

View file

@ -34,13 +34,14 @@ Page {
property int myUserId: tdLibWrapper.getUserInformation().id;
property bool isPrivateChat: false
property bool isSecretChat: false
property bool isBasicGroup: false
property bool isSuperGroup: false
property bool isChannel: false
property string chatPartnerGroupId
property bool userIsMember: (isPrivateChat && chatInformation["@type"]) || // should be optimized
property bool userIsMember: ((isPrivateChat || isSecretChat ) && chatInformation["@type"]) || // should be optimized
(isBasicGroup || isSuperGroup) && (
(groupInformation.status["@type"] === "chatMemberStatusMember")
|| (groupInformation.status["@type"] === "chatMemberStatusAdministrator")

View file

@ -36,8 +36,11 @@ Page {
property bool isInitialized: false;
readonly property int myUserId: tdLibWrapper.getUserInformation().id;
property var chatInformation;
property var secretChatDetails;
property alias chatPicture: chatPictureThumbnail.photoData
property bool isPrivateChat: false;
property bool isSecretChat: false;
property bool isSecretChatReady: false;
property bool isBasicGroup: false;
property bool isSuperGroup: false;
property bool isChannel: false;
@ -46,7 +49,7 @@ Page {
property int chatOnlineMemberCount: 0;
property var emojiProposals;
property bool iterativeInitialization: false;
readonly property bool userIsMember: (isPrivateChat && chatInformation["@type"]) || // should be optimized
readonly property bool userIsMember: ((isPrivateChat || isSecretChat) && chatInformation["@type"]) || // should be optimized
(isBasicGroup || isSuperGroup) && (
(chatGroupInformation.status["@type"] === "chatMemberStatusMember")
|| (chatGroupInformation.status["@type"] === "chatMemberStatusAdministrator")
@ -104,11 +107,21 @@ Page {
}
function updateChatPartnerStatusText() {
if(chatPage.state === "selectMessages") {
if (chatPage.state === "selectMessages") {
return
}
var statusText = Functions.getChatPartnerStatusText(chatPartnerInformation.status['@type'], chatPartnerInformation.status.was_online);
if(statusText) {
if (chatPage.secretChatDetails) {
var secretChatStatus = Functions.getSecretChatStatus(chatPage.secretChatDetails);
if (statusText && secretChatStatus) {
statusText += " - ";
}
if (secretChatStatus) {
statusText += secretChatStatus;
}
}
if (statusText) {
chatStatusText.text = statusText;
}
}
@ -138,12 +151,16 @@ Page {
chatView.currentIndex = -1;
chatView.lastReadSentIndex = 0;
var chatType = chatInformation.type['@type'];
isPrivateChat = ( chatType === "chatTypePrivate" );
isPrivateChat = chatType === "chatTypePrivate";
isSecretChat = chatType === "chatTypeSecret";
isBasicGroup = ( chatType === "chatTypeBasicGroup" );
isSuperGroup = ( chatType === "chatTypeSupergroup" );
if (isPrivateChat) {
if (isPrivateChat || isSecretChat) {
chatPartnerInformation = tdLibWrapper.getUserInformation(chatInformation.type.user_id);
updateChatPartnerStatusText();
if (isSecretChat) {
tdLibWrapper.getSecretChat(chatInformation.type.secret_chat_id);
}
}
else if (isBasicGroup) {
chatGroupInformation = tdLibWrapper.getBasicGroup(chatInformation.type.basic_group_id);
@ -291,11 +308,12 @@ Page {
|| groupStatusType === "chatMemberStatusAdministrator"
|| groupStatusType === "chatMemberStatusCreator"
|| (groupStatusType === "chatMemberStatusRestricted" && groupStatus.permissions[privilege])
|| (chatPage.isSecretChat && chatPage.isSecretChatReady)
}
function canPinMessages() {
Debug.log("Can we pin messages?");
if (chatPage.isPrivateChat) {
Debug.log("Private Chat: No!");
if (chatPage.isPrivateChat || chatPage.isSecretChat) {
Debug.log("Private/Secret Chat: No!");
return false;
}
if (chatPage.chatGroupInformation.status["@type"] === "chatMemberStatusCreator") {
@ -367,7 +385,7 @@ Page {
Connections {
target: tdLibWrapper
onUserUpdated: {
if (isPrivateChat && chatPartnerInformation.id.toString() === userId ) {
if ((isPrivateChat || isSecretChat) && chatPartnerInformation.id.toString() === userId ) {
chatPartnerInformation = userInformation;
updateChatPartnerStatusText();
}
@ -410,6 +428,22 @@ Page {
pinnedMessageItem.pinnedMessage = message;
}
}
onSecretChatReceived: {
if (secretChatId === chatInformation.type.secret_chat_id) {
Debug.log("[ChatPage] Received detailed information about this secret chat");
chatPage.secretChatDetails = secretChat;
updateChatPartnerStatusText();
chatPage.isSecretChatReady = chatPage.secretChatDetails.state["@type"] === "secretChatStateReady";
}
}
onSecretChatUpdated: {
if (secretChatId.toString() === chatInformation.type.secret_chat_id.toString()) {
Debug.log("[ChatPage] Detailed information about this secret chat was updated");
chatPage.secretChatDetails = secretChat;
updateChatPartnerStatusText();
chatPage.isSecretChatReady = chatPage.secretChatDetails.state["@type"] === "secretChatStateReady";
}
}
}
Connections {
@ -507,7 +541,7 @@ Page {
Timer {
id: chatContactTimeUpdater
interval: 60000
running: isPrivateChat
running: isPrivateChat || isSecretChat
repeat: true
onTriggered: {
updateChatPartnerStatusText();
@ -543,6 +577,19 @@ Page {
PullDownMenu {
visible: chatInformation.id !== chatPage.myUserId && !stickerPickerLoader.active && !messageOverlayLoader.active
MenuItem {
id: closeSecretChatMenuItem
visible: chatPage.isSecretChat && chatPage.secretChatDetails.state["@type"] !== "secretChatStateClosed"
onClicked: {
var remorse = Remorse.popupAction(appWindow, qsTr("Closing chat"), (function(secretChatId) {
return function() {
tdLibWrapper.closeSecretChat(secretChatId);
};
}(chatPage.secretChatDetails.id)))
}
text: qsTr("Close Chat")
}
MenuItem {
id: joinLeaveChatMenuItem
visible: (chatPage.isSuperGroup || chatPage.isBasicGroup) && chatGroupInformation && chatGroupInformation.status["@type"] !== "chatMemberStatusBanned"
@ -604,23 +651,49 @@ Page {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.paddingMedium
ProfileThumbnail {
id: chatPictureThumbnail
replacementStringHint: chatNameText.text
Item {
width: chatOverviewItem.height
height: chatOverviewItem.height
anchors.bottom: parent.bottom
anchors.bottomMargin: chatPage.isPortrait ? Theme.paddingMedium : Theme.paddingSmall
// Setting it directly may cause an stale state for the thumbnail in case the chat page
// was previously loaded with a picture and now it doesn't have one. Instead setting it
// when the ChatModel indicates a change. This also avoids flickering when the page is loaded...
Connections {
target: chatModel
onSmallPhotoChanged: {
chatPictureThumbnail.photoData = chatModel.smallPhoto;
ProfileThumbnail {
id: chatPictureThumbnail
replacementStringHint: chatNameText.text
width: parent.height
height: parent.height
// Setting it directly may cause an stale state for the thumbnail in case the chat page
// was previously loaded with a picture and now it doesn't have one. Instead setting it
// when the ChatModel indicates a change. This also avoids flickering when the page is loaded...
Connections {
target: chatModel
onSmallPhotoChanged: {
chatPictureThumbnail.photoData = chatModel.smallPhoto;
}
}
}
Rectangle {
id: chatSecretBackground
color: Theme.overlayBackgroundColor
width: chatPage.isPortrait ? Theme.fontSizeLarge : Theme.fontSizeMedium
height: width
anchors.left: parent.left
anchors.bottom: parent.bottom
radius: parent.width / 2
visible: chatPage.isSecretChat
}
Image {
id: chatSecretImage
source: "image://theme/icon-s-secure"
width: chatPage.isPortrait ? Theme.fontSizeSmall : Theme.fontSizeExtraSmall
height: width
anchors.centerIn: chatSecretBackground
visible: chatPage.isSecretChat
}
}
Item {
@ -855,8 +928,9 @@ Page {
VerticalScrollDecorator {}
ViewPlaceholder {
id: chatViewPlaceholder
enabled: chatView.count === 0
text: qsTr("This chat is empty.")
text: (chatPage.isSecretChat && !chatPage.isSecretChatReady) ? qsTr("This secret chat is not yet ready. Your chat partner needs to go online first.") : qsTr("This chat is empty.")
}
}
@ -1061,7 +1135,7 @@ Page {
}
}
IconButton {
visible: !chatPage.isPrivateChat && chatPage.hasSendPrivilege("can_send_polls")
visible: !(chatPage.isPrivateChat || chatPage.isSecretChat) && chatPage.hasSendPrivilege("can_send_polls")
icon.source: "image://theme/icon-m-question"
onClicked: {
pageStack.push(Qt.resolvedUrl("../pages/PollCreationPage.qml"), { "chatId" : chatInformation.id, groupName: chatInformation.title});
@ -1340,7 +1414,7 @@ Page {
verticalCenter: parent.verticalCenter
}
visible: selectedMessages.every(function(message){
return message.can_be_forwarded
return message.can_be_forwarded && !chatPage.isSecretChat
})
width: visible ? Theme.itemSizeMedium : 0
icon.source: "image://theme/icon-m-forward"

392
qml/pages/NewChatPage.qml Normal file
View file

@ -0,0 +1,392 @@
/*
Copyright (C) 2020 Sebastian J. Wolf and other contributors
This file is part of Fernschreiber.
Fernschreiber 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 3 of the License, or
(at your option) any later version.
Fernschreiber 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 Fernschreiber. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.6
import Sailfish.Silica 1.0
import "../components"
import "../js/twemoji.js" as Emoji
import "../js/functions.js" as Functions
Page {
id: newChatPage
allowedOrientations: Orientation.All
property bool isLoading: true;
function resetFocus() {
contactsSearchField.focus = false;
newChatPage.focus = true;
}
function reloadContacts() {
contactsModel.hydrateContacts();
contactsListView.model = contactsProxyModel;
newChatPage.isLoading = false;
}
onStatusChanged: {
if (status === PageStatus.Active) {
reloadContacts();
}
}
Connections {
target: tdLibWrapper
onContactsImported: {
reloadContacts();
appNotification.show(qsTr("Contacts successfully synchronized with Telegram."));
}
}
SilicaFlickable {
id: newChatContainer
contentHeight: newChatPage.height
anchors.fill: parent
PullDownMenu {
visible: contactsModel.canSynchronizeContacts()
MenuItem {
onClicked: {
newChatPage.isLoading = true;
if (!contactsModel.synchronizeContacts()) {
reloadContacts();
appNotification.show(qsTr("Could not synchronize your contacts with Telegram."));
}
// Success message is not fired before TDLib returned "Contacts imported" (see above)
}
text: qsTr("Synchronize Contacts with Telegram")
}
}
Column {
id: newChatPageColumn
width: newChatPage.width
height: newChatPage.height
PageHeader {
id: newChatPageHeader
title: qsTr("Your Contacts")
}
Item {
id: contactsItem
width: newChatPageColumn.width
height: newChatPageColumn.height - newChatPageHeader.height
Column {
visible: !newChatPage.isLoading
width: parent.width
height: parent.height
SearchField {
id: contactsSearchField
width: parent.width
placeholderText: qsTr("Search a contact...")
active: !newChatPage.isLoading
onTextChanged: {
contactsProxyModel.setFilterWildcard("*" + text + "*");
}
EnterKey.iconSource: "image://theme/icon-m-enter-close"
EnterKey.onClicked: {
resetFocus();
}
}
SilicaListView {
id: contactsListView
clip: true
width: parent.width
height: parent.height - contactsSearchField.height
visible: !newChatPage.isLoading
opacity: visible ? 1 : 0
Behavior on opacity { FadeAnimation {} }
signal newChatInitiated ( int currentIndex )
ViewPlaceholder {
y: Theme.paddingLarge
enabled: contactsListView.count === 0
text: qsTr("You don't have any contacts.")
}
delegate: Item {
id: newChatListItem
width: parent.width
height: contactListItem.height
PhotoTextsListItem {
id: contactListItem
opacity: visible ? 1 : 0
Behavior on opacity { FadeAnimation {} }
pictureThumbnail {
photoData: typeof photo_small !== "undefined" ? photo_small : {}
}
width: parent.width
primaryText.text: Emoji.emojify(title, primaryText.font.pixelSize, "../js/emoji/")
prologSecondaryText.text: "@" + ( username !== "" ? username : user_id )
tertiaryText {
maximumLineCount: 1
text: Functions.getChatPartnerStatusText(user_status, user_last_online);
}
onClicked: {
contactsListView.newChatInitiated(index);
}
Connections {
target: contactsListView
onNewChatInitiated: {
if (index === currentIndex) {
contactListItem.visible = false;
} else {
contactListItem.visible = true;
}
}
}
Connections {
target: contactsSearchField
onFocusChanged: {
if (contactsSearchField.focus) {
contactListItem.visible = true;
}
}
}
}
Column {
id: selectChatTypeColumn
visible: !contactListItem.visible
opacity: visible ? 1 : 0
Behavior on opacity { FadeAnimation {} }
width: parent.width
height: contactListItem.height
Item {
width: parent.width
height: parent.height - chatTypeSeparator.height
Rectangle {
anchors.fill: parent
opacity: Theme.opacityLow
color: Theme.overlayBackgroundColor
}
Item {
id: privateChatItem
height: parent.height
width: parent.width / 2
Rectangle {
id: privateChatHighlightBackground
anchors.fill: parent
color: Theme.highlightBackgroundColor
opacity: Theme.opacityHigh
visible: false
}
Row {
width: parent.width
height: parent.height - ( 2 * Theme.paddingSmall )
anchors.verticalCenter: parent.verticalCenter
IconButton {
id: privateChatButton
width: Theme.itemSizeLarge
height: Theme.itemSizeLarge
icon.source: "image://theme/icon-m-chat"
anchors.verticalCenter: parent.verticalCenter
onClicked: {
tdLibWrapper.createPrivateChat(display.id);
}
}
Column {
height: parent.height
width: parent.width - privateChatButton.width - Theme.horizontalPageMargin
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.paddingSmall
Text {
id: privateChatHeader
width: parent.width
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.ExtraBold
color: Theme.primaryColor
maximumLineCount: 1
elide: Text.ElideRight
textFormat: Text.StyledText
text: qsTr("Private Chat")
}
Text {
width: parent.width
height: parent.height - privateChatHeader.height - Theme.paddingSmall
font.pixelSize: Theme.fontSizeTiny
color: Theme.secondaryColor
wrapMode: Text.Wrap
elide: Text.ElideRight
textFormat: Text.StyledText
text: qsTr("Transport-encrypted, uses Telegram Cloud, sharable across devices")
}
}
}
MouseArea {
anchors.fill: parent
onClicked: {
tdLibWrapper.createPrivateChat(display.id);
}
onPressed: {
privateChatHighlightBackground.visible = true;
}
onReleased: {
privateChatHighlightBackground.visible = false;
}
}
}
Item {
id: secretChatItem
height: parent.height
width: parent.width / 2
anchors.left: privateChatItem.right
anchors.top: parent.top
Rectangle {
id: secretChatHighlightBackground
anchors.fill: parent
color: Theme.highlightBackgroundColor
opacity: Theme.opacityHigh
visible: false
}
Row {
width: parent.width
height: parent.height - ( 2 * Theme.paddingSmall )
anchors.verticalCenter: parent.verticalCenter
IconButton {
id: secretChatButton
width: Theme.itemSizeLarge
height: Theme.itemSizeLarge
icon.source: "image://theme/icon-m-device-lock"
anchors.verticalCenter: parent.verticalCenter
onClicked: {
tdLibWrapper.createNewSecretChat(display.id);
}
}
Column {
height: parent.height
width: parent.width - secretChatButton.width - Theme.horizontalPageMargin
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.paddingSmall
Text {
width: parent.width
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.ExtraBold
color: Theme.primaryColor
maximumLineCount: 1
elide: Text.ElideRight
textFormat: Text.StyledText
text: qsTr("Secret Chat")
}
Text {
width: parent.width
height: parent.height - privateChatHeader.height - Theme.paddingSmall
font.pixelSize: Theme.fontSizeTiny
color: Theme.secondaryColor
wrapMode: Text.Wrap
elide: Text.ElideRight
textFormat: Text.StyledText
text: qsTr("End-to-end-encrypted, accessible on this device only")
}
}
}
MouseArea {
anchors.fill: parent
onClicked: {
tdLibWrapper.createNewSecretChat(display.id);
}
onPressed: {
secretChatHighlightBackground.visible = true;
}
onReleased: {
secretChatHighlightBackground.visible = false;
}
}
}
}
Separator {
id: chatTypeSeparator
width: parent.width
color: Theme.primaryColor
horizontalAlignment: Qt.AlignHCenter
}
}
}
VerticalScrollDecorator {}
}
}
Column {
opacity: visible ? 1 : 0
Behavior on opacity { FadeAnimation {} }
visible: newChatPage.isLoading
width: parent.width
height: loadingLabel.height + loadingBusyIndicator.height + Theme.paddingMedium
spacing: Theme.paddingMedium
anchors.verticalCenter: parent.verticalCenter
InfoLabel {
id: loadingLabel
text: qsTr("Loading contacts...")
}
BusyIndicator {
id: loadingBusyIndicator
anchors.horizontalCenter: parent.horizontalCenter
running: newChatPage.isLoading
size: BusyIndicatorSize.Large
}
}
}
}
}
}

View file

@ -105,6 +105,7 @@ Page {
tdLibWrapper.getChats();
tdLibWrapper.getRecentStickers();
tdLibWrapper.getInstalledStickerSets();
tdLibWrapper.getContacts();
}
function initializePage() {
@ -197,6 +198,10 @@ Page {
text: qsTr("Settings")
onClicked: pageStack.push(Qt.resolvedUrl("../pages/SettingsPage.qml"))
}
MenuItem {
text: qsTr("New Chat")
onClicked: pageStack.push(Qt.resolvedUrl("../pages/NewChatPage.qml"))
}
}
Column {

View file

@ -46,6 +46,7 @@ namespace {
const QString IS_CHANNEL("is_channel");
const QString PINNED_MESSAGE_ID("pinned_message_id");
const QString _TYPE("@type");
const QString SECRET_CHAT_ID("secret_chat_id");
}
class ChatListModel::ChatData
@ -64,6 +65,7 @@ public:
RoleLastMessageText,
RoleLastMessageStatus,
RoleChatMemberStatus,
RoleSecretChatState,
RoleIsChannel
};
@ -86,6 +88,7 @@ public:
bool updateLastReadInboxMessageId(qlonglong messageId);
QVector<int> updateLastMessage(const QVariantMap &message);
QVector<int> updateGroup(const TDLibWrapper::Group *group);
QVector<int> updateSecretChat(const QVariantMap &secretChatDetails);
public:
QVariantMap chatData;
@ -94,6 +97,7 @@ public:
qlonglong groupId;
TDLibWrapper::ChatType chatType;
TDLibWrapper::ChatMemberStatus memberStatus;
TDLibWrapper::SecretChatState secretChatState;
QVariantMap userInformation;
};
@ -103,6 +107,7 @@ ChatListModel::ChatData::ChatData(const QVariantMap &data, const QVariantMap &us
order(data.value(ORDER).toLongLong()),
groupId(0),
memberStatus(TDLibWrapper::ChatMemberStatusUnknown),
secretChatState(TDLibWrapper::SecretChatStateUnknown),
userInformation(userInfo)
{
const QVariantMap type(data.value(TYPE).toMap());
@ -227,7 +232,11 @@ bool ChatListModel::ChatData::isHidden() const
break;
case TDLibWrapper::ChatTypeUnknown:
case TDLibWrapper::ChatTypePrivate:
break;
case TDLibWrapper::ChatTypeSecret:
if (secretChatState == TDLibWrapper::SecretChatStateClosed) {
return true;
}
break;
}
return false;
@ -288,6 +297,18 @@ QVector<int> ChatListModel::ChatData::updateGroup(const TDLibWrapper::Group *gro
return changedRoles;
}
QVector<int> ChatListModel::ChatData::updateSecretChat(const QVariantMap &secretChatDetails)
{
QVector<int> changedRoles;
TDLibWrapper::SecretChatState newSecretChatState = TDLibWrapper::secretChatStateFromString(secretChatDetails.value("state").toMap().value(_TYPE).toString());
if (newSecretChatState != secretChatState) {
secretChatState = newSecretChatState;
changedRoles.append(RoleSecretChatState);
}
return changedRoles;
}
ChatListModel::ChatListModel(TDLibWrapper *tdLibWrapper) : showHiddenChats(false)
{
this->tdLibWrapper = tdLibWrapper;
@ -302,6 +323,9 @@ ChatListModel::ChatListModel(TDLibWrapper *tdLibWrapper) : showHiddenChats(false
connect(tdLibWrapper, SIGNAL(chatNotificationSettingsUpdated(QString, QVariantMap)), this, SLOT(handleChatNotificationSettingsUpdated(QString, QVariantMap)));
connect(tdLibWrapper, SIGNAL(superGroupUpdated(qlonglong)), this, SLOT(handleGroupUpdated(qlonglong)));
connect(tdLibWrapper, SIGNAL(basicGroupUpdated(qlonglong)), this, SLOT(handleGroupUpdated(qlonglong)));
connect(tdLibWrapper, SIGNAL(secretChatUpdated(qlonglong, QVariantMap)), this, SLOT(handleSecretChatUpdated(qlonglong, QVariantMap)));
connect(tdLibWrapper, SIGNAL(secretChatReceived(qlonglong, QVariantMap)), this, SLOT(handleSecretChatUpdated(qlonglong, QVariantMap)));
connect(tdLibWrapper, SIGNAL(chatTitleUpdated(QString, QString)), this, SLOT(handleChatTitleUpdated(QString, QString)));
// Don't start the timer until we have at least one chat
relativeTimeRefreshTimer = new QTimer(this);
@ -332,6 +356,7 @@ QHash<int,QByteArray> ChatListModel::roleNames() const
roles.insert(ChatData::RoleLastMessageText, "last_message_text");
roles.insert(ChatData::RoleLastMessageStatus, "last_message_status");
roles.insert(ChatData::RoleChatMemberStatus, "chat_member_status");
roles.insert(ChatData::RoleSecretChatState, "secret_chat_state");
roles.insert(ChatData::RoleIsChannel, "is_channel");
return roles;
}
@ -359,6 +384,7 @@ QVariant ChatListModel::data(const QModelIndex &index, int role) const
case ChatData::RoleLastMessageDate: return data->senderMessageDate();
case ChatData::RoleLastMessageStatus: return data->senderMessageStatus();
case ChatData::RoleChatMemberStatus: return data->memberStatus;
case ChatData::RoleSecretChatState: return data->secretChatState;
case ChatData::RoleIsChannel: return data->isChannel();
}
}
@ -492,6 +518,38 @@ void ChatListModel::updateChatVisibility(const TDLibWrapper::Group *group)
}
}
void ChatListModel::updateSecretChatVisibility(const QVariantMap secretChatDetails)
{
LOG("Updating secret chat visibility" << secretChatDetails.value(ID).toString());
// See if any secret chat has been closed
for (int i = 0; i < chatList.size(); i++) {
ChatData *chat = chatList.at(i);
if (chat->chatType != TDLibWrapper::ChatTypeSecret) {
continue;
}
if (chat->chatData.value(TYPE).toMap().value(SECRET_CHAT_ID).toLongLong() != secretChatDetails.value(ID).toLongLong()) {
continue;
}
const QVector<int> changedRoles(chat->updateSecretChat(secretChatDetails));
if (chat->isHidden() && !showHiddenChats) {
LOG("Hiding chat" << chat->chatId << "at" << i);
beginRemoveRows(QModelIndex(), i, i);
chatList.removeAt(i);
// Update damaged part of the map
const int n = chatList.size();
for (int pos = i; pos < n; pos++) {
chatIndexMap.insert(chatList.at(pos)->chatId, pos);
}
i--;
hiddenChats.insert(chat->chatId, chat);
endRemoveRows();
} else if (!changedRoles.isEmpty()) {
const QModelIndex modelIndex(index(i));
emit dataChanged(modelIndex, modelIndex, changedRoles);
}
}
}
bool ChatListModel::showAllChats() const
{
return showHiddenChats;
@ -509,10 +567,19 @@ void ChatListModel::setShowAllChats(bool showAll)
void ChatListModel::handleChatDiscovered(const QString &, const QVariantMap &chatToBeAdded)
{
ChatData *chat = new ChatData(chatToBeAdded, tdLibWrapper->getUserInformation());
const TDLibWrapper::Group *group = tdLibWrapper->getGroup(chat->groupId);
if (group) {
chat->updateGroup(group);
}
if (chat->chatType == TDLibWrapper::ChatTypeSecret) {
QVariantMap secretChatDetails = tdLibWrapper->getSecretChatFromCache(chatToBeAdded.value(TYPE).toMap().value(SECRET_CHAT_ID).toLongLong());
if (!secretChatDetails.isEmpty()) {
chat->updateSecretChat(secretChatDetails);
}
}
if (chat->isHidden()) {
LOG("Hidden chat" << chat->chatId);
hiddenChats.insert(chat->chatId, chat);
@ -707,6 +774,33 @@ void ChatListModel::handleGroupUpdated(qlonglong groupId)
updateChatVisibility(tdLibWrapper->getGroup(groupId));
}
void ChatListModel::handleSecretChatUpdated(qlonglong secretChatId, const QVariantMap &secretChat)
{
LOG("Updating visibility of secret chat " << secretChatId);
updateSecretChatVisibility(secretChat);
}
void ChatListModel::handleChatTitleUpdated(const QString &chatId, const QString &title)
{
qlonglong chatIdLongLong = chatId.toLongLong();
if (chatIndexMap.contains(chatIdLongLong)) {
LOG("Updating title for" << chatId);
const int chatIndex = chatIndexMap.value(chatIdLongLong);
ChatData *chat = chatList.at(chatIndex);
chat->chatData.insert(TITLE, title);
QVector<int> changedRoles;
changedRoles.append(ChatData::RoleTitle);
const QModelIndex modelIndex(index(chatIndex));
emit dataChanged(modelIndex, modelIndex, changedRoles);
} else {
ChatData *chat = hiddenChats.value(chatId.toLongLong());
if (chat) {
LOG("Updating title for hidden chat" << chatId);
chat->chatData.insert(TITLE, title);
}
}
}
void ChatListModel::handleRelativeTimeRefreshTimer()
{
LOG("Refreshing timestamps");

View file

@ -54,6 +54,8 @@ private slots:
void handleMessageSendSucceeded(const QString &messageId, const QString &oldMessageId, const QVariantMap &message);
void handleChatNotificationSettingsUpdated(const QString &chatId, const QVariantMap &chatNotificationSettings);
void handleGroupUpdated(qlonglong groupId);
void handleSecretChatUpdated(qlonglong secretChatId, const QVariantMap &secretChat);
void handleChatTitleUpdated(const QString &chatId, const QString &title);
void handleRelativeTimeRefreshTimer();
signals:
@ -65,6 +67,7 @@ private:
class ChatData;
void addVisibleChat(ChatData *chat);
void updateChatVisibility(const TDLibWrapper::Group *group);
void updateSecretChatVisibility(const QVariantMap secretChatDetails);
int updateChatOrder(int chatIndex);
private:

184
src/contactsmodel.cpp Normal file
View file

@ -0,0 +1,184 @@
/*
Copyright (C) 2020 Sebastian J. Wolf and other contributors
This file is part of Fernschreiber.
Fernschreiber 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 3 of the License, or
(at your option) any later version.
Fernschreiber 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 Fernschreiber. If not, see <http://www.gnu.org/licenses/>.
*/
#include "contactsmodel.h"
#include <QListIterator>
#define DEBUG_MODULE ContactsModel
#include "debuglog.h"
namespace {
const QString STATUS("status");
const QString ID("id");
const QString TYPE("type");
const QString LAST_NAME("last_name");
const QString FIRST_NAME("first_name");
const QString USERNAME("username");
const QString _TYPE("@type");
const QString _EXTRA("@extra");
}
ContactsModel::ContactsModel(TDLibWrapper *tdLibWrapper, QObject *parent)
: QAbstractListModel(parent)
{
this->tdLibWrapper = tdLibWrapper;
connect(this->tdLibWrapper, SIGNAL(usersReceived(QString, QVariantList, int)), this, SLOT(handleUsersReceived(QString, QVariantList, int)));
this->deviceContactsDatabase = QSqlDatabase::addDatabase("QSQLITE", "contacts");
this->deviceContactsDatabase.setDatabaseName(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.local/share/system/Contacts/qtcontacts-sqlite/contacts.db");
if (this->deviceContactsDatabase.open()) {
LOG("Device's contacts database successfully opened :)");
this->canUseDeviceContacts = true;
} else {
LOG("Error opening device's contacts database :(");
this->canUseDeviceContacts = false;
}
}
QHash<int, QByteArray> ContactsModel::roleNames() const
{
QHash<int,QByteArray> roles;
roles.insert(ContactRole::RoleDisplay, "display");
roles.insert(ContactRole::RoleTitle, "title");
roles.insert(ContactRole::RoleUserId, "user_id");
roles.insert(ContactRole::RoleUsername, "username");
roles.insert(ContactRole::RolePhotoSmall, "photo_small");
roles.insert(ContactRole::RoleUserStatus, "user_status");
roles.insert(ContactRole::RoleUserLastOnline, "user_last_online");
roles.insert(ContactRole::RoleFilter, "filter");
return roles;
}
int ContactsModel::rowCount(const QModelIndex &) const
{
return this->contacts.size();
}
QVariant ContactsModel::data(const QModelIndex &index, int role) const
{
if (index.isValid()) {
QVariantMap requestedContact = contacts.value(index.row()).toMap();
switch (static_cast<ContactRole>(role)) {
case ContactRole::RoleDisplay: return requestedContact;
case ContactRole::RoleTitle: return QString(requestedContact.value("first_name").toString() + " " + requestedContact.value("last_name").toString()).trimmed();
case ContactRole::RoleUserId: return requestedContact.value("id");
case ContactRole::RoleUsername: return requestedContact.value("username");
case ContactRole::RolePhotoSmall: return requestedContact.value("profile_photo").toMap().value("small");
case ContactRole::RoleUserStatus: return requestedContact.value("status").toMap().value("@type");
case ContactRole::RoleUserLastOnline: return requestedContact.value("status").toMap().value("was_online");
case ContactRole::RoleFilter: return QString(requestedContact.value("first_name").toString() + " " + requestedContact.value("last_name").toString() + " " + requestedContact.value("username").toString()).trimmed();
}
}
return QVariant();
}
void ContactsModel::handleUsersReceived(const QString &extra, const QVariantList &userIds, int totalUsers)
{
if (extra == "contactsRequested") {
LOG("Received contacts list..." << totalUsers);
this->contactIds.clear();
QListIterator<QVariant> userIdIterator(userIds);
while (userIdIterator.hasNext()) {
QString nextUserId = userIdIterator.next().toString();
if (!this->tdLibWrapper->hasUserInformation(nextUserId)) {
this->tdLibWrapper->getUserFullInfo(nextUserId);
}
this->contactIds.append(nextUserId);
}
}
}
static bool compareUsers(const QVariant &user1, const QVariant &user2)
{
const QVariantMap userMap1 = user1.toMap();
const QVariantMap userMap2 = user2.toMap();
const QString lastName1 = userMap1.value(LAST_NAME).toString();
const QString lastName2 = userMap2.value(LAST_NAME).toString();
if (!lastName1.isEmpty()) {
if (lastName1 < lastName2) {
return true;
} else if (lastName1 > lastName2) {
return false;
}
}
const QString firstName1 = userMap1.value(FIRST_NAME).toString();
const QString firstName2 = userMap2.value(FIRST_NAME).toString();
if (firstName1 < firstName2) {
return true;
} else if (firstName1 > firstName2) {
return false;
}
const QString userName1 = userMap1.value(USERNAME).toString();
const QString userName2 = userMap2.value(USERNAME).toString();
if (userName1 < userName2) {
return true;
} else if (userName1 > userName2) {
return false;
}
return userMap1.value(ID).toLongLong() < userMap2.value(ID).toLongLong();
}
void ContactsModel::hydrateContacts()
{
LOG("Hydrating contacts...");
this->contacts.clear();
QListIterator<QString> userIdIterator(contactIds);
while (userIdIterator.hasNext()) {
QString nextUserId = userIdIterator.next();
LOG("Hydrating contact:" << nextUserId);
this->contacts.append(this->tdLibWrapper->getUserInformation(nextUserId));
}
LOG("Hydrated contacts:" << this->contacts.size());
std::sort(this->contacts.begin(), this->contacts.end(), compareUsers);
}
bool ContactsModel::synchronizeContacts()
{
LOG("Synchronizing device contacts");
QVariantList deviceContacts;
QSqlQuery databaseQuery(this->deviceContactsDatabase);
databaseQuery.prepare("select distinct c.contactId, c.firstName, c.lastName, n.phoneNumber from Contacts as c inner join PhoneNumbers as n on c.contactId = n.contactId where n.phoneNumber is not null and ( c.firstName is not null or c.lastName is not null );");
if (databaseQuery.exec()) {
LOG("Device contacts successfully selected from database!");
while (databaseQuery.next()) {
QVariantMap singleContact;
singleContact.insert("first_name", databaseQuery.value(1).toString());
singleContact.insert("last_name", databaseQuery.value(2).toString());
singleContact.insert("phone_number", databaseQuery.value(3).toString());
deviceContacts.append(singleContact);
LOG("Found contact" << singleContact.value("first_name").toString() << singleContact.value("last_name").toString() << singleContact.value("phone_number").toString());
}
if (!deviceContacts.isEmpty()) {
LOG("Importing found contacts" << deviceContacts.size());
this->tdLibWrapper->importContacts(deviceContacts);
}
return true;
} else {
LOG("Error selecting contacts from database!");
return false;
}
}
bool ContactsModel::canSynchronizeContacts()
{
return this->canUseDeviceContacts;
}

68
src/contactsmodel.h Normal file
View file

@ -0,0 +1,68 @@
/*
Copyright (C) 2020 Sebastian J. Wolf and other contributors
This file is part of Fernschreiber.
Fernschreiber 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 3 of the License, or
(at your option) any later version.
Fernschreiber 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 Fernschreiber. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTSMODEL_H
#define CONTACTSMODEL_H
#include <QAbstractListModel>
#include <QVariantList>
#include <QSqlDatabase>
#include <QSqlQuery>
#include "tdlibwrapper.h"
class ContactsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum ContactRole {
RoleDisplay = Qt::DisplayRole,
RolePhotoSmall,
RoleTitle,
RoleUserId,
RoleUsername,
RoleUserStatus,
RoleUserLastOnline,
RoleFilter
};
ContactsModel(TDLibWrapper *tdLibWrapper, QObject *parent = nullptr);
virtual QHash<int,QByteArray> roleNames() const override;
virtual int rowCount(const QModelIndex &) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
Q_INVOKABLE void hydrateContacts();
Q_INVOKABLE bool synchronizeContacts();
Q_INVOKABLE bool canSynchronizeContacts();
public slots:
void handleUsersReceived(const QString &extra, const QVariantList &userIds, int totalUsers);
private:
TDLibWrapper *tdLibWrapper;
QVariantList contacts;
QList<QString> contactIds;
QString filter;
QSqlDatabase deviceContactsDatabase;
bool canUseDeviceContacts;
};
#endif // CONTACTSMODEL_H

View file

@ -10,6 +10,10 @@ FernschreiberUtils::FernschreiberUtils(QObject *parent) : QObject(parent)
QString FernschreiberUtils::getMessageShortText(const QVariantMap &messageContent, const bool &myself)
{
if (messageContent.isEmpty()) {
return QString();
}
QString contentType = messageContent.value("@type").toString();
if (contentType == "messageText") {

View file

@ -42,6 +42,7 @@
#include "stickermanager.h"
#include "tgsplugin.h"
#include "fernschreiberutils.h"
#include "contactsmodel.h"
// The default filter can be overridden by QT_LOGGING_RULES envinronment variable, e.g.
// QT_LOGGING_RULES="fernschreiber.*=true" harbour-fernschreiber
@ -96,6 +97,14 @@ int main(int argc, char *argv[])
StickerManager stickerManager(tdLibWrapper);
context->setContextProperty("stickerManager", &stickerManager);
ContactsModel contactsModel(tdLibWrapper, view.data());
context->setContextProperty("contactsModel", &contactsModel);
QSortFilterProxyModel contactsProxyModel(view.data());
contactsProxyModel.setSourceModel(&contactsModel);
contactsProxyModel.setFilterRole(ContactsModel::RoleFilter);
contactsProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
context->setContextProperty("contactsProxyModel", &contactsProxyModel);
view->setSource(SailfishApp::pathTo("qml/harbour-fernschreiber.qml"));
view->show();
return app->exec();

View file

@ -44,6 +44,7 @@ namespace {
const QString UNREAD_COUNT("unread_count");
const QString LAST_READ_INBOX_MESSAGE_ID("last_read_inbox_message_id");
const QString LAST_READ_OUTBOX_MESSAGE_ID("last_read_outbox_message_id");
const QString SECRET_CHAT("secret_chat");
const QString TYPE("@type");
const QString EXTRA("@extra");
@ -126,6 +127,9 @@ TDLibReceiver::TDLibReceiver(void *tdLibClient, QObject *parent) : QThread(paren
handlers.insert("users", &TDLibReceiver::processUsers);
handlers.insert("error", &TDLibReceiver::processError);
handlers.insert("ok", &TDLibReceiver::nop);
handlers.insert("secretChat", &TDLibReceiver::processSecretChat);
handlers.insert("updateSecretChat", &TDLibReceiver::processUpdateSecretChat);
handlers.insert("importedContacts", &TDLibReceiver::processImportedContacts);
}
void TDLibReceiver::setActive(bool active)
@ -523,3 +527,22 @@ void TDLibReceiver::processError(const QVariantMap &receivedInformation)
void TDLibReceiver::nop(const QVariantMap &)
{
}
void TDLibReceiver::processSecretChat(const QVariantMap &receivedInformation)
{
LOG("Received a secret chat");
emit secretChat(receivedInformation.value(ID).toLongLong(), receivedInformation);
}
void TDLibReceiver::processUpdateSecretChat(const QVariantMap &receivedInformation)
{
LOG("A secret chat was updated");
QVariantMap updatedSecretChat = receivedInformation.value(SECRET_CHAT).toMap();
emit secretChatUpdated(updatedSecretChat.value(ID).toLongLong(), updatedSecretChat);
}
void TDLibReceiver::processImportedContacts(const QVariantMap &receivedInformation)
{
LOG("Contacts were imported");
emit contactsImported(receivedInformation.value("importer_count").toList(), receivedInformation.value("user_ids").toList());
}

View file

@ -85,6 +85,9 @@ signals:
void chatPinnedMessageUpdated(qlonglong chatId, qlonglong pinnedMessageId);
void usersReceived(const QString &extra, const QVariantList &userIds, int totalUsers);
void errorReceived(const int code, const QString &message);
void secretChat(qlonglong secretChatId, const QVariantMap &secretChat);
void secretChatUpdated(qlonglong secretChatId, const QVariantMap &secretChat);
void contactsImported(const QVariantList &importerCount, const QVariantList &userIds);
private:
typedef void (TDLibReceiver::*Handler)(const QVariantMap &);
@ -145,6 +148,9 @@ private:
void processUsers(const QVariantMap &receivedInformation);
void processError(const QVariantMap &receivedInformation);
void nop(const QVariantMap &receivedInformation);
void processSecretChat(const QVariantMap &receivedInformation);
void processUpdateSecretChat(const QVariantMap &receivedInformation);
void processImportedContacts(const QVariantMap &receivedInformation);
};
#endif // TDLIBRECEIVER_H

View file

@ -37,8 +37,10 @@ namespace {
const QString STATUS("status");
const QString ID("id");
const QString TYPE("type");
const QString LAST_NAME("last_name");
const QString FIRST_NAME("first_name");
const QString USERNAME("username");
const QString VALUE("value");
const QString _TYPE("@type");
const QString _EXTRA("@extra");
}
@ -94,6 +96,8 @@ TDLibWrapper::TDLibWrapper(AppSettings *appSettings, MceInterface *mceInterface,
connect(this->tdLibReceiver, SIGNAL(messagesDeleted(QString, QVariantList)), this, SIGNAL(messagesDeleted(QString, QVariantList)));
connect(this->tdLibReceiver, SIGNAL(chats(QVariantMap)), this, SIGNAL(chatsReceived(QVariantMap)));
connect(this->tdLibReceiver, SIGNAL(chat(QVariantMap)), this, SLOT(handleChatReceived(QVariantMap)));
connect(this->tdLibReceiver, SIGNAL(secretChat(qlonglong, QVariantMap)), this, SLOT(handleSecretChatReceived(qlonglong, QVariantMap)));
connect(this->tdLibReceiver, SIGNAL(secretChatUpdated(qlonglong, QVariantMap)), this, SLOT(handleSecretChatUpdated(qlonglong, QVariantMap)));
connect(this->tdLibReceiver, SIGNAL(recentStickersUpdated(QVariantList)), this, SIGNAL(recentStickersUpdated(QVariantList)));
connect(this->tdLibReceiver, SIGNAL(stickers(QVariantList)), this, SIGNAL(stickersReceived(QVariantList)));
connect(this->tdLibReceiver, SIGNAL(installedStickerSetsUpdated(QVariantList)), this, SIGNAL(installedStickerSetsUpdated(QVariantList)));
@ -113,6 +117,7 @@ TDLibWrapper::TDLibWrapper(AppSettings *appSettings, MceInterface *mceInterface,
connect(this->tdLibReceiver, SIGNAL(chatPinnedMessageUpdated(qlonglong, qlonglong)), this, SIGNAL(chatPinnedMessageUpdated(qlonglong, qlonglong)));
connect(this->tdLibReceiver, SIGNAL(usersReceived(QString, QVariantList, int)), this, SIGNAL(usersReceived(QString, QVariantList, int)));
connect(this->tdLibReceiver, SIGNAL(errorReceived(int, QString)), this, SIGNAL(errorReceived(int, QString)));
connect(this->tdLibReceiver, SIGNAL(contactsImported(QVariantList, QVariantList)), this, SIGNAL(contactsImported(QVariantList, QVariantList)));
connect(&emojiSearchWorker, SIGNAL(searchCompleted(QString, QVariantList)), this, SLOT(handleEmojiSearchCompleted(QString, QVariantList)));
@ -615,7 +620,7 @@ void TDLibWrapper::getGroupFullInfo(const QString &groupId, bool isSuperGroup)
void TDLibWrapper::getUserFullInfo(const QString &userId)
{
LOG("Retrieving UserFullInfo");
LOG("Retrieving UserFullInfo" << userId);
QVariantMap requestObject;
requestObject.insert(_TYPE, "getUserFullInfo");
requestObject.insert(_EXTRA, userId);
@ -633,6 +638,16 @@ void TDLibWrapper::createPrivateChat(const QString &userId)
this->sendRequest(requestObject);
}
void TDLibWrapper::createNewSecretChat(const QString &userId)
{
LOG("Creating new secret chat");
QVariantMap requestObject;
requestObject.insert(_TYPE, "createNewSecretChat");
requestObject.insert("user_id", userId);
requestObject.insert(_EXTRA, "openDirectly"); //gets matched in qml
this->sendRequest(requestObject);
}
void TDLibWrapper::createSupergroupChat(const QString &supergroupId)
{
LOG("Creating Supergroup Chat");
@ -802,6 +817,42 @@ void TDLibWrapper::getDeepLinkInfo(const QString &link)
this->sendRequest(requestObject);
}
void TDLibWrapper::getContacts()
{
LOG("Retrieving contacts");
QVariantMap requestObject;
requestObject.insert(_TYPE, "getContacts");
requestObject.insert(_EXTRA, "contactsRequested");
this->sendRequest(requestObject);
}
void TDLibWrapper::getSecretChat(qlonglong secretChatId)
{
LOG("Getting detailed information about secret chat" << secretChatId);
QVariantMap requestObject;
requestObject.insert(_TYPE, "getSecretChat");
requestObject.insert("secret_chat_id", secretChatId);
this->sendRequest(requestObject);
}
void TDLibWrapper::closeSecretChat(qlonglong secretChatId)
{
LOG("Closing secret chat" << secretChatId);
QVariantMap requestObject;
requestObject.insert(_TYPE, "closeSecretChat");
requestObject.insert("secret_chat_id", secretChatId);
this->sendRequest(requestObject);
}
void TDLibWrapper::importContacts(const QVariantList &contacts)
{
LOG("Importing contacts");
QVariantMap requestObject;
requestObject.insert(_TYPE, "importContacts");
requestObject.insert("contacts", contacts);
this->sendRequest(requestObject);
}
void TDLibWrapper::searchEmoji(const QString &queryString)
{
LOG("Searching emoji" << queryString);
@ -823,6 +874,11 @@ QVariantMap TDLibWrapper::getUserInformation(const QString &userId)
return this->allUsers.value(userId).toMap();
}
bool TDLibWrapper::hasUserInformation(const QString &userId)
{
return this->allUsers.contains(userId);
}
QVariantMap TDLibWrapper::getUserInformationByName(const QString &userName)
{
return this->allUserNames.value(userName).toMap();
@ -868,6 +924,11 @@ QVariantMap TDLibWrapper::getChat(const QString &chatId)
return this->chats.value(chatId).toMap();
}
QVariantMap TDLibWrapper::getSecretChatFromCache(qlonglong secretChatId)
{
return this->secretChats.value(secretChatId);
}
QString TDLibWrapper::getOptionString(const QString &optionName)
{
return this->options.value(optionName).toString();
@ -1137,6 +1198,18 @@ void TDLibWrapper::handleOpenWithChanged()
}
}
void TDLibWrapper::handleSecretChatReceived(qlonglong secretChatId, const QVariantMap &secretChat)
{
this->secretChats.insert(secretChatId, secretChat);
emit secretChatReceived(secretChatId, secretChat);
}
void TDLibWrapper::handleSecretChatUpdated(qlonglong secretChatId, const QVariantMap &secretChat)
{
this->secretChats.insert(secretChatId, secretChat);
emit secretChatUpdated(secretChatId, secretChat);
}
void TDLibWrapper::handleStorageOptimizerChanged()
{
setOptionBoolean("use_storage_optimizer", appSettings->storageOptimizer());
@ -1154,7 +1227,7 @@ void TDLibWrapper::setInitialParameters()
initialParameters.insert("use_file_database", true);
initialParameters.insert("use_chat_info_database", true);
initialParameters.insert("use_message_database", true);
initialParameters.insert("use_secret_chats", false);
initialParameters.insert("use_secret_chats", true);
initialParameters.insert("system_language_code", QLocale::system().name());
QSettings hardwareSettings("/etc/hw-release", QSettings::NativeFormat);
initialParameters.insert("device_model", hardwareSettings.value("NAME", "Unknown Mobile Device").toString());
@ -1289,7 +1362,15 @@ TDLibWrapper::ChatMemberStatus TDLibWrapper::chatMemberStatusFromString(const QS
(status == QStringLiteral("chatMemberStatusAdministrator")) ? ChatMemberStatusAdministrator :
(status == QStringLiteral("chatMemberStatusRestricted")) ? ChatMemberStatusRestricted :
(status == QStringLiteral("chatMemberStatusBanned")) ? ChatMemberStatusBanned :
ChatMemberStatusUnknown;
ChatMemberStatusUnknown;
}
TDLibWrapper::SecretChatState TDLibWrapper::secretChatStateFromString(const QString &state)
{
return (state == QStringLiteral("secretChatStateClosed")) ? SecretChatStateClosed :
(state == QStringLiteral("secretChatStatePending")) ? SecretChatStatePending :
(state == QStringLiteral("secretChatStateReady")) ? SecretChatStateReady :
SecretChatStateUnknown;
}
TDLibWrapper::ChatMemberStatus TDLibWrapper::Group::chatMemberStatus() const

View file

@ -79,6 +79,14 @@ public:
};
Q_ENUM(ChatMemberStatus)
enum SecretChatState {
SecretChatStateUnknown,
SecretChatStateClosed,
SecretChatStatePending,
SecretChatStateReady,
};
Q_ENUM(SecretChatState)
class Group {
public:
Group(qlonglong id) : groupId(id) { }
@ -94,12 +102,14 @@ public:
Q_INVOKABLE TDLibWrapper::ConnectionState getConnectionState();
Q_INVOKABLE QVariantMap getUserInformation();
Q_INVOKABLE QVariantMap getUserInformation(const QString &userId);
Q_INVOKABLE bool hasUserInformation(const QString &userId);
Q_INVOKABLE QVariantMap getUserInformationByName(const QString &userName);
Q_INVOKABLE QVariantMap getUnreadMessageInformation();
Q_INVOKABLE QVariantMap getUnreadChatInformation();
Q_INVOKABLE QVariantMap getBasicGroup(qlonglong groupId) const;
Q_INVOKABLE QVariantMap getSuperGroup(qlonglong groupId) const;
Q_INVOKABLE QVariantMap getChat(const QString &chatId);
Q_INVOKABLE QVariantMap getSecretChatFromCache(qlonglong secretChatId);
Q_INVOKABLE QString getOptionString(const QString &optionName);
Q_INVOKABLE void copyFileToDownloads(const QString &filePath);
Q_INVOKABLE void openFileOnDevice(const QString &filePath);
@ -146,6 +156,7 @@ public:
Q_INVOKABLE void getGroupFullInfo(const QString &groupId, bool isSuperGroup);
Q_INVOKABLE void getUserFullInfo(const QString &userId);
Q_INVOKABLE void createPrivateChat(const QString &userId);
Q_INVOKABLE void createNewSecretChat(const QString &userId);
Q_INVOKABLE void createSupergroupChat(const QString &supergroupId);
Q_INVOKABLE void createBasicGroupChat(const QString &basicGroupId);
Q_INVOKABLE void getGroupsInCommon(const QString &userId, int limit, int offset);
@ -162,6 +173,10 @@ public:
Q_INVOKABLE void searchPublicChat(const QString &userName);
Q_INVOKABLE void joinChatByInviteLink(const QString &inviteLink);
Q_INVOKABLE void getDeepLinkInfo(const QString &link);
Q_INVOKABLE void getContacts();
Q_INVOKABLE void getSecretChat(qlonglong secretChatId);
Q_INVOKABLE void closeSecretChat(qlonglong secretChatId);
Q_INVOKABLE void importContacts(const QVariantList &contacts);
// Others (candidates for extraction ;))
Q_INVOKABLE void searchEmoji(const QString &queryString);
@ -172,6 +187,7 @@ public:
const Group* getGroup(qlonglong groupId) const;
static ChatType chatTypeFromString(const QString &type);
static ChatMemberStatus chatMemberStatusFromString(const QString &status);
static SecretChatState secretChatStateFromString(const QString &state);
signals:
void versionDetected(const QString &version);
@ -205,6 +221,8 @@ signals:
void messagesDeleted(const QString &chatId, const QVariantList &messageIds);
void chatsReceived(const QVariantMap &chats);
void chatReceived(const QVariantMap &chat);
void secretChatReceived(qlonglong secretChatId, const QVariantMap &secretChat);
void secretChatUpdated(qlonglong secretChatId, const QVariantMap &secretChat);
void recentStickersUpdated(const QVariantList &stickerIds);
void stickersReceived(const QVariantList &stickers);
void installedStickerSetsUpdated(const QVariantList &stickerSetIds);
@ -225,6 +243,7 @@ signals:
void chatPinnedMessageUpdated(qlonglong chatId, qlonglong pinnedMessageId);
void usersReceived(const QString &extra, const QVariantList &userIds, int totalUsers);
void errorReceived(const int code, const QString &message);
void contactsImported(const QVariantList &importerCount, const QVariantList &userIds);
public slots:
void handleVersionDetected(const QString &version);
@ -243,6 +262,8 @@ public slots:
void handleStickerSets(const QVariantList &stickerSets);
void handleEmojiSearchCompleted(const QString &queryString, const QVariantList &resultList);
void handleOpenWithChanged();
void handleSecretChatReceived(qlonglong secretChatId, const QVariantMap &secretChat);
void handleSecretChatUpdated(qlonglong secretChatId, const QVariantMap &secretChat);
void handleStorageOptimizerChanged();
private:
@ -267,6 +288,7 @@ private:
QVariantMap allUsers;
QVariantMap allUserNames;
QVariantMap chats;
QMap<qlonglong, QVariantMap> secretChats;
QVariantMap unreadMessageInformation;
QVariantMap unreadChatInformation;
QHash<qlonglong,Group*> basicGroups;

View file

@ -179,6 +179,10 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation>Neuer geheimer Chat</translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -260,6 +264,10 @@
<source>Mark all messages as read</source>
<translation>Nachrichten als gelesen markieren</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation>Keine Nachricht in diesem Chat</translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -375,6 +383,18 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation>Dieser geheime Chat ist noch nicht bereit. Ihr Chatpartner muss erst noch online gehen.</translation>
</message>
<message>
<source>Closing chat</source>
<translation>Schließe Chat</translation>
</message>
<message>
<source>Close Chat</source>
<translation>Chat schließen</translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -903,6 +923,53 @@
<translation>Diese Nachricht wurde weitergeleitet. Ursprünglicher Autor: %1</translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation>Ihre Kontakte</translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation>Sie haben keine Kontakte</translation>
</message>
<message>
<source>Private Chat</source>
<translation>Privater Chat</translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation>Transportverschlüsselt, nutzt Telegram-Cloud, teilbar zwischen Geräten</translation>
</message>
<message>
<source>Secret Chat</source>
<translation>Geheimer Chat</translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation>Ende-zu-Ende-verschlüsselt, nur auf diesem Gerät zugreifbar</translation>
</message>
<message>
<source>Search a contact...</source>
<translation>Einen Kontakt suchen...</translation>
</message>
<message>
<source>Loading contacts...</source>
<translation>Lade Kontakte...</translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation>Kontakte mit Telegram synchronisieren</translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation>Konnte Ihre Kontakte nicht mit Telegram synchronisieren.</translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation>Die Kontakte wurden erfolgreich mit Telegram synchronisiert.</translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -955,6 +1022,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation>Sie haben noch keine Chats.</translation>
</message>
<message>
<source>New Chat</source>
<translation>Neuer Chat</translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1625,5 +1696,13 @@
<source>You are already a member of this chat.</source>
<translation>Sie sind bereits Mitglied dieses Chats.</translation>
</message>
<message>
<source>Closed!</source>
<translation>Geschlossen!</translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation>Ausstehende Bestätigung</translation>
</message>
</context>
</TS>

View file

@ -179,6 +179,10 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation>New Secret Chat</translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -260,6 +264,10 @@
<source>Mark all messages as read</source>
<translation>Mark all messages as read</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation>No message in this chat.</translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -375,6 +383,18 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation>This secret chat is not yet ready. Your chat partner needs to go online first.</translation>
</message>
<message>
<source>Closing chat</source>
<translation>Closing chat</translation>
</message>
<message>
<source>Close Chat</source>
<translation>Close Chat</translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -903,6 +923,53 @@
<translation>This message was forwarded. Original author: %1</translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation>Your Contacts</translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation>You don&apos;t have any contacts.</translation>
</message>
<message>
<source>Private Chat</source>
<translation>Private Chat</translation>
</message>
<message>
<source>Secret Chat</source>
<translation>Secret Chat</translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation>End-to-end-encrypted, accessible on this device only</translation>
</message>
<message>
<source>Search a contact...</source>
<translation>Search a contact...</translation>
</message>
<message>
<source>Loading contacts...</source>
<translation>Loading contacts...</translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation>Transport-encrypted, uses Telegram Cloud, sharable across devices</translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation>Synchronize Contacts with Telegram</translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation>Could not synchronize your contacts with Telegram.</translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation>Contacts successfully synchronized with Telegram.</translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -955,6 +1022,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation>You don&apos;t have any chats yet.</translation>
</message>
<message>
<source>New Chat</source>
<translation>New Chat</translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1625,5 +1696,13 @@
<source>You are already a member of this chat.</source>
<translation>You are already a member of this chat.</translation>
</message>
<message>
<source>Closed!</source>
<translation>Closed!</translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation>Pending acknowledgement</translation>
</message>
</context>
</TS>

View file

@ -176,6 +176,10 @@
<numerusform>%1 en línea</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -257,6 +261,10 @@
<source>Mark all messages as read</source>
<translation>Marcar todos como leídos</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -365,6 +373,18 @@
<numerusform>%1 en línea</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -893,6 +913,53 @@
<translation>Este mensaje fue reenviado. Autor original: %1</translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -944,6 +1011,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation>No hay todavía ninguna charla.</translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1606,5 +1677,13 @@
<source>You are already a member of this chat.</source>
<translation>Ya eres miembro de este grupo.</translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -179,6 +179,10 @@
<numerusform>%1 paikalla</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -260,6 +264,10 @@
<source>Mark all messages as read</source>
<translation>Merkitse kaikki viestit luetuiksi</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -375,6 +383,18 @@
<numerusform>%1 paikalla</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -904,6 +924,53 @@
<translation>Välitetty viesti. Alkuperäinen lähettäjä: %1</translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -956,6 +1023,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation>Sinulla ei ole vielä keskusteluja.</translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1626,5 +1697,13 @@
<source>You are already a member of this chat.</source>
<translation>Olet jo tämän ryhmän jäsen.</translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -176,6 +176,10 @@
<numerusform></numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -257,6 +261,10 @@
<source>Mark all messages as read</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -365,6 +373,18 @@
<numerusform></numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -893,6 +913,53 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -944,6 +1011,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1606,5 +1677,13 @@
<source>You are already a member of this chat.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -179,6 +179,10 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -260,6 +264,10 @@
<source>Mark all messages as read</source>
<translation>Segna tutti i messaggi come già letti</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -375,6 +383,18 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -903,6 +923,53 @@
<translation>Questo è un messaggio inoltrato. Autore originale: %1</translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -955,6 +1022,10 @@
<source>Loading chat list...</source>
<translation>Carica lista chat...</translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1625,5 +1696,13 @@
<source>You are already a member of this chat.</source>
<translation>Sei già membro di questa chat.</translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -182,6 +182,10 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -263,6 +267,10 @@
<source>Mark all messages as read</source>
<translation>Zaznacz wszystkie wiadomości jako przeczytane</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -385,6 +393,18 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -913,6 +933,53 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -966,6 +1033,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation>Nie masz jeszcze żadnych czatów.</translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1644,5 +1715,13 @@
<source>You are already a member of this chat.</source>
<translation>Jesteś już członkiem tego czatu.</translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -182,6 +182,10 @@
<numerusform></numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -263,6 +267,10 @@
<source>Mark all messages as read</source>
<translation>Отметить все сообщения как прочитанные</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -385,6 +393,18 @@
<numerusform></numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -913,6 +933,53 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -966,6 +1033,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation>Тут пока ничего нет</translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1644,5 +1715,13 @@
<source>You are already a member of this chat.</source>
<translation>Вы уже в этом чате.</translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -179,6 +179,10 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -260,6 +264,10 @@
<source>Mark all messages as read</source>
<translation>Markera alla meddelanden som lästa</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -375,6 +383,18 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -903,6 +923,53 @@
<translation>Detta meddelande är vidarebefordrat. Ursprunglig avsändare: %1</translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -955,6 +1022,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation>Du har inga chattar än.</translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1625,5 +1696,13 @@
<source>You are already a member of this chat.</source>
<translation>Du är redan medlem i den här chatten.</translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -176,6 +176,10 @@
<numerusform>%1 线</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -257,6 +261,10 @@
<source>Mark all messages as read</source>
<translation></translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -365,6 +373,18 @@
<numerusform>%1 线</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -893,6 +913,53 @@
<translation>: %1</translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -944,6 +1011,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation></translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1606,5 +1677,13 @@
<source>You are already a member of this chat.</source>
<translation></translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>

View file

@ -179,6 +179,10 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>New Secret Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatInformationTabItemMembersGroups</name>
@ -260,6 +264,10 @@
<source>Mark all messages as read</source>
<translation>Mark all messages as read</translation>
</message>
<message>
<source>No message in this chat.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatPage</name>
@ -375,6 +383,18 @@
<numerusform>%1 online</numerusform>
</translation>
</message>
<message>
<source>This secret chat is not yet ready. Your chat partner needs to go online first.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Closing chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Close Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ChatSelectionPage</name>
@ -903,6 +923,53 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NewChatPage</name>
<message>
<source>Your Contacts</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>You don&apos;t have any contacts.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Private Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Secret Chat</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>End-to-end-encrypted, accessible on this device only</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Loading contacts...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Transport-encrypted, uses Telegram Cloud, sharable across devices</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Search a contact...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Synchronize Contacts with Telegram</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not synchronize your contacts with Telegram.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Contacts successfully synchronized with Telegram.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>NotificationManager</name>
<message numerus="yes">
@ -955,6 +1022,10 @@
<source>You don&apos;t have any chats yet.</source>
<translation>You don&apos;t have any chats yet.</translation>
</message>
<message>
<source>New Chat</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>PinnedMessageItem</name>
@ -1625,5 +1696,13 @@
<source>You are already a member of this chat.</source>
<translation>You are already a member of this chat.</translation>
</message>
<message>
<source>Closed!</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Pending acknowledgement</source>
<translation type="unfinished"></translation>
</message>
</context>
</TS>