Merge lp:messaging-app/staging into lp:messaging-app

Proposed by Tiago Salem Herrmann
Status: Approved
Approved by: Gustavo Pichorim Boiko
Approved revision: 662
Proposed branch: lp:messaging-app/staging
Merge into: lp:messaging-app
Diff against target: 4652 lines (+2264/-682)
39 files modified
CMakeLists.txt (+1/-0)
accounts/CMakeLists.txt (+2/-0)
accounts/messaging-app.application (+13/-0)
debian/control (+1/-0)
debian/messaging-app-apparmor.additions (+4/-0)
debian/messaging-app.install (+1/-0)
debian/rules (+6/-1)
src/main.cpp (+2/-2)
src/messagingapplication.cpp (+21/-1)
src/messagingapplication.h (+1/-0)
src/qml/ComposeBar.qml (+127/-16)
src/qml/ContactSearchList.qml (+59/-77)
src/qml/ContactSearchWidget.qml (+26/-9)
src/qml/Dialogs/InformationDialog.qml (+11/-6)
src/qml/FavoriteChannels.qml (+93/-0)
src/qml/GroupChatInfoPage.qml (+179/-146)
src/qml/MainPage.qml (+105/-31)
src/qml/MessageBubble.qml (+7/-0)
src/qml/MessageDelegate.qml (+25/-6)
src/qml/MessageInfoDialog.qml (+67/-32)
src/qml/Messages.qml (+277/-97)
src/qml/MessagesListView.qml (+10/-1)
src/qml/MessagingContactEditorPage.qml (+9/-2)
src/qml/MessagingContactViewPage.qml (+51/-18)
src/qml/MultiRecipientInput.qml (+41/-30)
src/qml/NewGroupPage.qml (+61/-20)
src/qml/NewRecipientPage.qml (+73/-9)
src/qml/OnlineAccountsHelper.qml (+91/-0)
src/qml/ParticipantDelegate.qml (+3/-2)
src/qml/ParticipantInfoPage.qml (+80/-60)
src/qml/ParticipantsPopover.qml (+126/-34)
src/qml/RegularMessageDelegate.qml (+1/-5)
src/qml/RegularMessageDelegate_irc.qml (+200/-0)
src/qml/SettingsPage.qml (+239/-46)
src/qml/ThreadDelegate.qml (+51/-12)
src/qml/ThreadsSectionDelegate.qml (+7/-1)
src/qml/TransparentButton.qml (+17/-1)
src/qml/messaging-app.qml (+156/-15)
tests/qml/tst_MessagesView.qml (+20/-2)
To merge this branch: bzr merge lp:messaging-app/staging
Reviewer Review Type Date Requested Status
Gustavo Pichorim Boiko (community) Approve
system-apps-ci-bot continuous-integration Approve
Review via email: mp+320736@code.launchpad.net

Commit message

- Allow join existing channels.
- Register messaging-app on online-accounts as IRC application.
- Fix argument parsing with the messaging:number pattern.
- Always show generic accounts, even when there is only one account.
- Enable return key to send messages.
- Fix attachments previewer.
- Hide attachment options when requested.
- Enable rejoin button when channel isn't active.
- Change some strings to match protocol specific terms.
- Select first account if no phone accounts exist and generic accounts are available
- Leave room channels on close
- Refactory focus and keyboard navigation
- Use 'Tab' to auto-complete nicknames.
- Add fallback to nicknames
- Use message delegates based on protocol if that exists.
- Close channels when threads are removed.
- Disable contact matching dynamically depending on the addressable fields.
- Mark entire threads as read when opening a channel
- Allow user to select the sort field for conversation list.
- Improve participants screen performance
- Add a configuration option to disconnect from server on application exit.
- Allow to favorite a channels and store it on settings.
- Mark thread as not connected (opacity = 0.5)
- Adapt to fetch the participants when needed.
- Add apparmor rules to access dri.

Description of the change

- Allow join existing channels.
- Register messaging-app on online-accounts as IRC application.
- Fix argument parsing with the messaging:number pattern.
- Always show generic accounts, even when there is only one account.
- Enable return key to send messages.
- Fix attachments previewer.
- Hide attachment options when requested.
- Enable rejoin button when channel isn't active.
- Change some strings to match protocol specific terms.
- Select first account if no phone accounts exist and generic accounts are available
- Leave room channels on close
- Refactory focus and keyboard navigation
- Use 'Tab' to auto-complete nicknames.
- Add fallback to nicknames
- Use message delegates based on protocol if that exists.
- Close channels when threads are removed.
- Disable contact matching dynamically depending on the addressable fields.
- Mark entire threads as read when opening a channel
- Allow user to select the sort field for conversation list.
- Improve participants screen performance
- Add a configuration option to disconnect from server on application exit.
- Allow to favorite a channels and store it on settings.
- Mark thread as not connected (opacity = 0.5)
- Adapt to fetch the participants when needed.
- Add apparmor rules to access dri.

To post a comment you must log in.
Revision history for this message
system-apps-ci-bot (system-apps-ci-bot) wrote :

PASSED: Continuous integration, rev:662
https://jenkins.canonical.com/system-apps/job/lp-messaging-app-ci/1/
Executed test runs:
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build/2348
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-0-fetch/2348
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/2166
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=xenial+overlay/2166/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=zesty/2166
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=amd64,release=zesty/2166/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/2166
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=xenial+overlay/2166/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=zesty/2166
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=armhf,release=zesty/2166/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/2166
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=xenial+overlay/2166/artifact/output/*zip*/output.zip
    SUCCESS: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=zesty/2166
        deb: https://jenkins.canonical.com/system-apps/job/build-2-binpkg/arch=i386,release=zesty/2166/artifact/output/*zip*/output.zip

Click here to trigger a rebuild:
https://jenkins.canonical.com/system-apps/job/lp-messaging-app-ci/1/rebuild

review: Approve (continuous-integration)
Revision history for this message
Gustavo Pichorim Boiko (boiko) wrote :

The changes were reviewed individually and tested from this branch, so approving.

review: Approve
lp:messaging-app/staging updated
663. By Tiago Salem Herrmann

Check if 'callback' is undefined to avoid crashing the app

Unmerged revisions

663. By Tiago Salem Herrmann

Check if 'callback' is undefined to avoid crashing the app

662. By Tiago Salem Herrmann

Add apparmor rules to access dri.

661. By Tiago Salem Herrmann

Adapt to fetch the participants when needed.

660. By Tiago Salem Herrmann

- Add a configuration option to disconnect from server on application exit.
- Allow to favorite a channels and store it on settings.
- Mark thread as not connected (opacity = 0.5)

659. By Tiago Salem Herrmann

Improve participants screen performance

658. By Tiago Salem Herrmann

Allow user to select the sort field for conversation list.

657. By Tiago Salem Herrmann

Mark entire threads as read when opening a channel

656. By Tiago Salem Herrmann

Disable contact matching dynamically depending on the addressable fields.

655. By Tiago Salem Herrmann

Close channels when threads are removed.

654. By Tiago Salem Herrmann

Use message delegates based on protocol if that exists.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2016-06-08 18:35:43 +0000
3+++ CMakeLists.txt 2017-03-27 19:24:23 +0000
4@@ -71,6 +71,7 @@
5
6 add_subdirectory(po)
7 add_subdirectory(src)
8+add_subdirectory(accounts)
9 if (NOT ("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "aarch64") AND (NOT ("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64el")) AND (NOT ("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "ppc64le")))
10 add_subdirectory(tests)
11 endif()
12
13=== added directory 'accounts'
14=== added file 'accounts/CMakeLists.txt'
15--- accounts/CMakeLists.txt 1970-01-01 00:00:00 +0000
16+++ accounts/CMakeLists.txt 2017-03-27 19:24:23 +0000
17@@ -0,0 +1,2 @@
18+file(GLOB APPLICATION_FILES *.application)
19+install(FILES ${APPLICATION_FILES} DESTINATION share/accounts/applications/)
20
21=== added file 'accounts/messaging-app.application'
22--- accounts/messaging-app.application 1970-01-01 00:00:00 +0000
23+++ accounts/messaging-app.application 2017-03-27 19:24:23 +0000
24@@ -0,0 +1,13 @@
25+<?xml version="1.0" encoding="UTF-8"?>
26+<application id="messaging-app">
27+ <description>Messaging</description>
28+ <desktop-entry>messaging-app.desktop</desktop-entry>
29+ <translations>messaging</translations>
30+ <profile>messaging-app</profile>
31+
32+ <services>
33+ <service id="telephony-irc-im">
34+ <description>Integrate your IRC accounts</description>
35+ </service>
36+ </services>
37+</application>
38
39=== modified file 'debian/control'
40--- debian/control 2016-11-05 00:46:55 +0000
41+++ debian/control 2017-03-27 19:24:23 +0000
42@@ -51,6 +51,7 @@
43 qtdeclarative5-ubuntu-telephony-phonenumber0.1,
44 qtdeclarative5-ubuntu-history0.1 | qtdeclarative5-ubuntu-history-plugin,
45 qtdeclarative5-ubuntu-telephony0.1 | qtdeclarative5-ubuntu-telephony-plugin,
46+ qtdeclarative5-ubuntu-keyboard-extensions0.1,
47 qtdeclarative5-qtcontacts-plugin,
48 qtdeclarative5-gsettings1.0,
49 qml-module-qt-labs-settings,
50
51=== modified file 'debian/messaging-app-apparmor.additions'
52--- debian/messaging-app-apparmor.additions 2016-01-11 12:10:08 +0000
53+++ debian/messaging-app-apparmor.additions 2017-03-27 19:24:23 +0000
54@@ -12,6 +12,10 @@
55 bus=session
56 peer=(name=com.canonical.TelephonyServiceIndicator,label=unconfined),
57
58+ dbus bind
59+ bus=session
60+ name=com.canonical.MessagingApp,
61+
62 # make it possible for apps to register a telepathy observer
63 dbus bind
64 bus=session
65
66=== modified file 'debian/messaging-app.install'
67--- debian/messaging-app.install 2016-05-20 04:32:06 +0000
68+++ debian/messaging-app.install 2017-03-27 19:24:23 +0000
69@@ -1,3 +1,4 @@
70+usr/share/accounts/applications/*
71 usr/share/applications/messaging-app*.desktop
72 usr/share/url-dispatcher/urls
73 usr/share/content-hub/peers/messaging-app
74
75=== modified file 'debian/rules'
76--- debian/rules 2016-07-29 23:26:31 +0000
77+++ debian/rules 2017-03-27 19:24:23 +0000
78@@ -26,7 +26,12 @@
79 sed 's,Apps/@{APP_PKGNAME},Apps/messaging-app,g' | \
80 sed '/lttng-ust-/c\ \/{,var\/}run\/shm\/lttng-ust-* r,' | \
81 sed '/dconf.user rw/c\ \/run\/user\/\[0-9\]*\/dconf\/user rw,' | \
82- sed 's,deny owner .*dconf/user r,owner @\{HOME\}/.config/dconf/user r,g' \
83+ sed 's,deny owner .*dconf/user r,owner @\{HOME\}/.config/dconf/user r,g' | \
84+ egrep -v 'deny /run/udev/data/\*\* r,' | \
85+ sed 's#^}$$#\n /sys/class/ r,\n /sys/class/input/ r,\n /run/udev/data/** r,\n}#g' | \
86+ egrep -v '^\s*deny /dev/ r,\s*$$' | \
87+ sed 's#^\(\s*\)deny\(\s\+/{run,dev}/shm/pulse-shm\*\s\+w,\).*$$#\1owner\2#g' | \
88+ sed 's#^}$$#\n /dev/dri/ r,\n /sys/devices/pci[0-9]*/**/config r,\n}#g' \
89 > ./debian/usr.bin.messaging-app
90 (head -n -2 ./debian/usr.bin.messaging-app; cat ./debian/messaging-app-apparmor.additions; \
91 echo } ) > ./debian/usr.bin.messaging-app2
92
93=== modified file 'src/main.cpp'
94--- src/main.cpp 2014-08-26 19:07:12 +0000
95+++ src/main.cpp 2017-03-27 19:24:23 +0000
96@@ -42,8 +42,8 @@
97 // as it doesn’t play well with QtFolks.
98 int main(int argc, char** argv)
99 {
100- QGuiApplication::setApplicationName("Messaging App");
101- QGuiApplication::setOrganizationName("com.ubuntu.messaging-app");
102+ QCoreApplication::setOrganizationName("com.ubuntu.messaging-app");
103+ QCoreApplication::setApplicationName("MessagingApp");
104
105 MessagingApplication application(argc, argv);
106
107
108=== modified file 'src/messagingapplication.cpp'
109--- src/messagingapplication.cpp 2016-10-17 13:02:38 +0000
110+++ src/messagingapplication.cpp 2017-03-27 19:24:23 +0000
111@@ -89,7 +89,6 @@
112 MessagingApplication::MessagingApplication(int &argc, char **argv)
113 : QGuiApplication(argc, argv), m_view(0), m_applicationIsReady(false)
114 {
115- setApplicationName("MessagingApp");
116 }
117
118 bool MessagingApplication::fullscreen() const
119@@ -100,6 +99,7 @@
120
121 bool MessagingApplication::setup()
122 {
123+ QDBusConnection::sessionBus().registerService("com.canonical.MessagingApp");
124 installIconPath();
125 static QList<QString> validSchemes;
126 bool fullScreen = false;
127@@ -258,6 +258,8 @@
128 QStringList participantIds = value.split(";");
129 properties["participantIds"] = participantIds;
130 }
131+ } else {
132+ properties["participantIds"] = QStringList() << value;
133 }
134 QUrlQuery query(url);
135 Q_FOREACH(const Pair &item, query.queryItems(QUrl::FullyDecoded)) {
136@@ -368,3 +370,21 @@
137 {
138 return findRecursiveChild(m_view->rootObject(), objectName, property, value);
139 }
140+
141+// Check if a delegate file exists with protocol name as suffix
142+// If true use the specialized delegate
143+// If false use the default delegate
144+QUrl MessagingApplication::delegateFromProtocol(const QUrl &delegate, const QString &protocol)
145+{
146+ if (protocol.isEmpty())
147+ return delegate;
148+
149+ QString localFile = delegate.toLocalFile();
150+ QString fileNameWithProtocol = QString("%1_%2.qml")
151+ .arg(localFile.mid(0, localFile.lastIndexOf(".")))
152+ .arg(protocol.toLower());
153+
154+ if (QFile::exists(fileNameWithProtocol))
155+ return fileNameWithProtocol;
156+ return delegate;
157+}
158
159=== modified file 'src/messagingapplication.h'
160--- src/messagingapplication.h 2016-07-28 04:14:35 +0000
161+++ src/messagingapplication.h 2017-03-27 19:24:23 +0000
162@@ -46,6 +46,7 @@
163 void showNotificationMessage(const QString &message, const QString &icon = QString());
164 QObject *findMessagingChild(const QString &objectName);
165 QObject *findMessagingChild(const QString &objectName, const QString &property, const QVariant &value);
166+ QUrl delegateFromProtocol(const QUrl &delegate, const QString &protocol);
167
168 private Q_SLOTS:
169 void setFullscreen(bool fullscreen);
170
171=== modified file 'src/qml/ComposeBar.qml'
172--- src/qml/ComposeBar.qml 2016-11-07 17:21:02 +0000
173+++ src/qml/ComposeBar.qml 2017-03-27 19:24:23 +0000
174@@ -43,6 +43,10 @@
175 property alias inputMethodComposing: messageTextArea.inputMethodComposing
176 property bool usingMMS: false
177 property bool isBroadcast: false
178+ property bool returnToSend: false
179+ property bool enableAttachments: true
180+ property alias participants: participantPopover.participants
181+ readonly property alias textArea: messageTextArea
182
183 onRecordingChanged: {
184 if (recording) {
185@@ -63,7 +67,8 @@
186 }
187
188 function forceFocus() {
189- messageTextArea.forceActiveFocus()
190+ if (showContents)
191+ messageTextArea.forceActiveFocus()
192 }
193
194 function reset() {
195@@ -192,14 +197,14 @@
196 }
197
198 Behavior on opacity { UbuntuNumberAnimation {} }
199- visible: opacity > 0
200+ visible: opacity > 0 && composeBar.enableAttachments
201
202- width: childrenRect.width
203- height: childrenRect.height
204+ width: visible ? childrenRect.width : 0
205+ height: visible ? childrenRect.height : 0
206
207 anchors {
208 left: parent.left
209- leftMargin: units.gu(2)
210+ leftMargin: visible ? units.gu(2) : 0
211 verticalCenter: sendButton.verticalCenter
212 }
213 spacing: units.gu(2)
214@@ -408,11 +413,93 @@
215 TextArea {
216 id: messageTextArea
217 objectName: "messageTextArea"
218+
219+ property bool autoCompleteLock: false
220+
221+ property int autoCompleteStartIndex: -1
222+
223+ function updateAutoComplete(startIndex, input)
224+ {
225+ var showPopup = false
226+ if (input.charAt(startIndex) === "@") {
227+ showPopup = true
228+ startIndex += 1
229+ }
230+
231+ var autoCompletePrefix = input.slice(startIndex, input.length)
232+
233+ return participantPopover.showParticpantsStartWith(composeBar, autoCompletePrefix, showPopup)
234+ }
235+
236+ function autoComplete()
237+ {
238+ autoCompleteLock = true
239+ var suggestion = ""
240+ var lastSpace = -1
241+
242+ var autoCompleteText = text
243+ if (participantPopover.active) {
244+ if (participantPopover.popupVisible) {
245+ suggestion = updateAutoComplete(autoCompleteStartIndex, autoCompleteText)
246+ } else {
247+ suggestion = participantPopover.nextItem()
248+ }
249+ } else if (autoCompleteText.length > 0) {
250+ autoCompleteStartIndex = autoCompleteText.lastIndexOf(" ") + 1
251+ suggestion = updateAutoComplete(autoCompleteStartIndex, autoCompleteText)
252+ forceFocus()
253+ } else {
254+ autoCompleteLock = false
255+ return false
256+ }
257+
258+ if (suggestion.length > 0) {
259+ var sliceEnd = autoCompleteText.charAt(autoCompleteStartIndex) === "@" ? autoCompleteStartIndex + 1 : autoCompleteStartIndex
260+ messageTextArea.text = text.slice(0, sliceEnd) + suggestion + ", "
261+ if (participantPopover.popupVisible) {
262+ messageTextArea.select(autoCompleteText.length, text.length)
263+ } else {
264+ messageTextArea.cursorPosition = messageTextArea.text.length
265+ }
266+
267+ } else {
268+ participantPopover.close()
269+ autoCompleteStartIndex = -1
270+ }
271+
272+ autoCompleteLock = false
273+ return true
274+ }
275+
276+ onTextChanged: {
277+ if (autoCompleteLock)
278+ return
279+
280+ // non-visual popover does not care about text change
281+ if (!participantPopover.popupVisible) {
282+ participantPopover.close()
283+ autoCompleteStartIndex = -1
284+ return
285+ }
286+
287+ if (autoCompleteStartIndex != -1)
288+ autoComplete()
289+ }
290+
291+ function returnPressed() {
292+ if (composeBar.returnToSend) {
293+ sendButton.processSend()
294+ return true
295+ }
296+ return false
297+ }
298 anchors {
299 top: parent.top
300 left: parent.left
301 right: parent.right
302 }
303+ Keys.onReturnPressed: event.accepted = returnPressed()
304+ Keys.onEnterPressed: event.accepted = returnPressed()
305 // this value is to avoid letter being cut off
306 height: units.gu(4.3)
307 style: LocalTextAreaStyle {}
308@@ -438,6 +525,21 @@
309 font.family: "Ubuntu"
310 font.pixelSize: FontUtils.sizeToPixels("medium")
311 color: Theme.palette.normal.backgroundText
312+ Keys.onPressed: {
313+ if (event.key === Qt.Key_Tab) {
314+ event.accepted = autoComplete()
315+ } else if (participantPopover.popupVisible) {
316+ // cancel non-visual autocomplete if any other key is pressed
317+ participantPopover.close()
318+ autoCompleteStartIndex = -1
319+ }
320+ }
321+
322+ Keys.onReleased: {
323+ if (event.key === Qt.Key_At) {
324+ event.accepted = autoComplete()
325+ }
326+ }
327 }
328
329 // show the counts if option is enabled, and more than one line
330@@ -578,15 +680,7 @@
331 anchors.rightMargin: units.gu(2)
332 iconSource: Qt.resolvedUrl("./assets/send.svg")
333 enabled: !recordButton.enabled
334- onEnabledChanged: {
335- if (enabled) {
336- enableSendButton.start()
337- }
338- }
339- opacity: 0
340- visible: enabled
341-
342- onClicked: {
343+ function processSend() {
344 // make sure we flush everything we have prepared in the OSK preedit
345 Qt.inputMethod.commit();
346 if ((textEntry.text == "" && attachments.count == 0) || !canSend) {
347@@ -599,6 +693,17 @@
348
349 composeBar.sendRequested(textEntry.text, attachments)
350 }
351+ onEnabledChanged: {
352+ if (enabled) {
353+ enableSendButton.start()
354+ }
355+ }
356+ opacity: composeBar.enableAttachments ? 0 : 1
357+ visible: enabled
358+
359+ onClicked: {
360+ processSend()
361+ }
362 }
363
364 TransparentButton {
365@@ -611,7 +716,7 @@
366 rightMargin: units.gu(2)
367 }
368
369- enabled: textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0 ? false : true
370+ enabled: textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0 || !composeBar.enableAttachments ? false : true
371 onEnabledChanged: {
372 if (enabled) {
373 enableRecordButton.start()
374@@ -653,6 +758,12 @@
375 drag.axis: Drag.XAxis
376 drag.minimumX: (leftSideActions.x + leftSideActions.width)
377 drag.maximumX: recordButton.x
378-
379+ }
380+
381+ ParticipantsPopover {
382+ id: participantPopover
383+
384+ height: parent.parent.height
385+ width: parent.width
386 }
387 }
388
389=== modified file 'src/qml/ContactSearchList.qml'
390--- src/qml/ContactSearchList.qml 2016-10-18 13:08:39 +0000
391+++ src/qml/ContactSearchList.qml 2017-03-27 19:24:23 +0000
392@@ -20,21 +20,26 @@
393 import QtContacts 5.0
394
395 import Ubuntu.Components 1.3
396-import Ubuntu.Components.ListItems 1.3 as ListItem
397 import Ubuntu.Contacts 0.1
398
399-ListView {
400+UbuntuListView {
401 id: root
402
403 // FIXME: change the Ubuntu.Contacts model to search for more fields
404 property alias filterTerm: contactModel.filterTerm
405- onFilterTermChanged: console.debug("FILTER :" + filterTerm)
406
407 signal contactPicked(string identifier, string label, string avatar)
408-
409- model: ContactListModel {
410+ signal focusUp()
411+
412+ ContactDetailPhoneNumberTypeModel {
413+ id: phoneTypeModel
414+ }
415+
416+ ContactListModel {
417 id: contactModel
418
419+ property var proxyModel: []
420+
421 manager: "galera"
422 view: root
423 autoUpdate: false
424@@ -61,84 +66,61 @@
425 detailTypesHint: [ ContactDetail.DisplayLabel,
426 ContactDetail.PhoneNumber ]
427 }
428- }
429-
430- ContactDetailPhoneNumberTypeModel {
431- id: phoneTypeModel
432- }
433-
434- delegate: Item {
435+
436+ onContactsChanged: {
437+ var proxy = []
438+ for (var i=0; i < contacts.length; i++) {
439+ for (var p=0; p < contacts[i].phoneNumbers.length; p++) {
440+ proxy.push({"contact": contacts[i], "phoneIndex": p})
441+ }
442+ }
443+ contactModel.proxyModel = proxy
444+ }
445+ }
446+
447+ model: contactModel.proxyModel
448+ delegate: ListItem {
449 anchors {
450 left: parent.left
451 right: parent.right
452 }
453- height: phoneRepeater.count * units.gu(6)
454- Column {
455- anchors.fill: parent
456-
457- Repeater {
458- id: phoneRepeater
459-
460- model: contact.phoneNumbers.length
461-
462- delegate: MouseArea {
463- anchors {
464- left: parent.left
465- right: parent.right
466- }
467- height: units.gu(6)
468-
469- onClicked: root.contactPicked(contact.phoneNumbers[index].number, contact.displayLabel.label, contact.avatar.url)
470-
471- Column {
472- anchors.right: parent.right
473- anchors.left: parent.left
474- anchors.verticalCenter: parent.verticalCenter
475- height: childrenRect.height
476- spacing: units.gu(.5)
477-
478- Label {
479- anchors {
480- left: parent.left
481- leftMargin: units.gu(2)
482- right: parent.right
483- }
484- height: units.gu(2)
485- text: {
486- // this is necessary to keep the string in the original format
487- var originalText = contact.displayLabel.label
488- var lowerSearchText = filterTerm.toLowerCase()
489- var lowerText = originalText.toLowerCase()
490- var searchIndex = lowerText.indexOf(lowerSearchText)
491- if (searchIndex !== -1) {
492- var piece = originalText.substr(searchIndex, lowerSearchText.length)
493- return originalText.replace(piece, "<b>" + piece + "</b>")
494- } else {
495- return originalText
496- }
497- }
498- fontSize: "medium"
499- color: Theme.palette.normal.backgroundText
500- }
501- Label {
502- anchors {
503- left: parent.left
504- leftMargin: units.gu(2)
505- right: parent.right
506- }
507- height: units.gu(2)
508- text: {
509- var phoneDetail = contact.phoneNumbers[index]
510- return ("%1 %2").arg(phoneTypeModel.get(phoneTypeModel.getTypeIndex(phoneDetail)).label)
511- .arg(phoneDetail.number)
512- }
513- color: Theme.palette.normal.backgroundSecondaryText
514- }
515-
516- ListItem.ThinDivider {}
517- }
518+ height: itemLayout.height
519+
520+ onClicked: root.contactPicked(modelData.contact.phoneNumbers[modelData.phoneIndex].number,
521+ modelData.contact.displayLabel.label, modelData.contact.avatar.url)
522+
523+ ListItemLayout {
524+ id: itemLayout
525+
526+ title.text: {
527+ // this is necessary to keep the string in the original format
528+ var originalText = modelData.contact.displayLabel.label
529+ var lowerSearchText = filterTerm.toLowerCase()
530+ var lowerText = originalText.toLowerCase()
531+ var searchIndex = lowerText.indexOf(lowerSearchText)
532+ if (searchIndex !== -1) {
533+ var piece = originalText.substr(searchIndex, lowerSearchText.length)
534+ return originalText.replace(piece, "<b>" + piece + "</b>")
535+ } else {
536+ return originalText
537 }
538 }
539+ title.fontSize: "medium"
540+ title.color: Theme.palette.normal.backgroundText
541+
542+ subtitle.text: {
543+ var phoneDetail = modelData.contact.phoneNumbers[modelData.phoneIndex]
544+ return ("%1 %2").arg(phoneTypeModel.get(phoneTypeModel.getTypeIndex(phoneDetail)).label)
545+ .arg(phoneDetail.number)
546+ }
547+ subtitle.color: Theme.palette.normal.backgroundSecondaryText
548 }
549 }
550+
551+ Keys.onUpPressed: {
552+ if (currentIndex == 0)
553+ focusUp()
554+
555+ event.accepted = false
556+ }
557 }
558
559=== modified file 'src/qml/ContactSearchWidget.qml'
560--- src/qml/ContactSearchWidget.qml 2016-08-25 21:00:04 +0000
561+++ src/qml/ContactSearchWidget.qml 2017-03-27 19:24:23 +0000
562@@ -20,7 +20,7 @@
563 import Ubuntu.Components 1.3
564 import Ubuntu.Components.ListItems 1.3 as ListItems
565
566-Item {
567+FocusScope {
568 id: searchItem
569 property alias text: contactSearch.text
570 property alias hasFocus: contactSearch.focus
571@@ -28,6 +28,7 @@
572 property int searchResultsHeight: 0
573
574 signal contactPicked(string identifier, string alias, string avatar)
575+
576 anchors {
577 left: parent.left
578 right: parent.right
579@@ -42,8 +43,20 @@
580 anchors.verticalCenter: contactSearch.verticalCenter
581 text: i18n.tr("Members:")
582 }
583+
584+ onContactPicked: contactSearch.forceActiveFocus()
585+
586 TextField {
587 id: contactSearch
588+
589+ function commit()
590+ {
591+ if (text == "")
592+ return
593+ searchItem.contactPicked(text, "","")
594+ text = ""
595+ }
596+
597 anchors.top: parent.top
598 anchors.left: membersLabel.right
599 anchors.leftMargin: units.gu(1)
600@@ -53,16 +66,13 @@
601 hasClearButton: false
602 placeholderText: i18n.tr("Number or contact name")
603 inputMethodHints: Qt.ImhNoPredictiveText
604- Keys.onReturnPressed: {
605- if (text == "")
606- return
607- searchItem.contactPicked(text, "","")
608- text = ""
609- }
610+ focus: true
611+ Keys.onReturnPressed: commit()
612+ Keys.onEnterPressed: commit()
613+ Keys.onDownPressed: searchListLoader.item.forceActiveFocus()
614
615 Icon {
616 name: "add"
617- color: Theme.palette.normal.backgroundText
618 height: units.gu(2)
619 anchors {
620 right: parent.right
621@@ -75,8 +85,8 @@
622 Qt.inputMethod.hide()
623 mainStack.addPageToCurrentColumn(searchItem.parentPage, Qt.resolvedUrl("NewRecipientPage.qml"), {"itemCallback": searchItem.parentPage})
624 }
625- z: 2
626 }
627+ z: 2
628 }
629 }
630 Loader {
631@@ -111,5 +121,12 @@
632 item.contactPicked.connect(searchItem.contactPicked)
633 }
634 }
635+
636+ Connections {
637+ target: searchListLoader.item
638+ onFocusUp: {
639+ contactSearch.forceActiveFocus()
640+ }
641+ }
642 }
643 }
644
645=== modified file 'src/qml/Dialogs/InformationDialog.qml'
646--- src/qml/Dialogs/InformationDialog.qml 2016-07-13 20:42:55 +0000
647+++ src/qml/Dialogs/InformationDialog.qml 2017-03-27 19:24:23 +0000
648@@ -16,7 +16,7 @@
649 * along with this program. If not, see <http://www.gnu.org/licenses/>.
650 */
651
652-import QtQuick 2.0
653+import QtQuick 2.4
654 import Ubuntu.Components 1.3
655 import Ubuntu.Components.Popups 1.3
656
657@@ -26,12 +26,17 @@
658 objectName: "informationDialog"
659 Button {
660 objectName: "closeInformationDialog"
661- text: i18n.tr("Close")
662+ action: Action {
663+ text: i18n.tr("Close")
664+ shortcut: "Esc"
665+ onTriggered: {
666+ PopupUtils.close(dialogue)
667+ Qt.inputMethod.hide()
668+ }
669+ }
670 color: UbuntuColors.orange
671- onClicked: {
672- PopupUtils.close(dialogue)
673- Qt.inputMethod.hide()
674- }
675+ Component.onCompleted: forceActiveFocus()
676 }
677+
678 }
679 }
680
681=== added file 'src/qml/FavoriteChannels.qml'
682--- src/qml/FavoriteChannels.qml 1970-01-01 00:00:00 +0000
683+++ src/qml/FavoriteChannels.qml 2017-03-27 19:24:23 +0000
684@@ -0,0 +1,93 @@
685+/*
686+ * Copyright 2012-2016 Canonical Ltd.
687+ *
688+ * This file is part of messaging-app.
689+ *
690+ * messaging-app is free software; you can redistribute it and/or modify
691+ * it under the terms of the GNU General Public License as published by
692+ * the Free Software Foundation; version 3.
693+ *
694+ * messaging-app is distributed in the hope that it will be useful,
695+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
696+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
697+ * GNU General Public License for more details.
698+ *
699+ * You should have received a copy of the GNU General Public License
700+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
701+ */
702+
703+import QtQuick 2.2
704+import Qt.labs.settings 1.0
705+
706+
707+Item {
708+ property var _favoritesByAccount: []
709+
710+
711+ function getFavoriteChannels(account)
712+ {
713+ var settings = settingsFromAccount(account)
714+ return favoriteChannels(settings)
715+ }
716+
717+ function favoriteIndex(account, fav)
718+ {
719+ return getFavoriteChannels(account).indexOf(fav)
720+ }
721+
722+ function isFavorite(account, fav)
723+ {
724+ return (favoriteIndex(account, fav) !== -1)
725+ }
726+
727+ function addFavorite(account, fav) {
728+ var settings = settingsFromAccount(account)
729+ if (favoriteIndexFromSettings(settings, fav) === -1) {
730+ settings.favoriteChannels += ";" + fav
731+ }
732+ }
733+
734+ function removeFavorite(account, fav) {
735+ var settings = settingsFromAccount(account)
736+ var index = favoriteIndexFromSettings(settings, fav)
737+ if (index !== -1) {
738+ var list = settings.favoriteChannels.split(";")
739+ list.splice(index, 1)
740+ settings.favoriteChannels = list.join(";")
741+ }
742+ }
743+
744+ // private
745+ function settingsFromAccount(account) {
746+ var account_ = account.replace(/\//g,"#")
747+ if (account_ in _favoritesByAccount) {
748+ return _favoritesByAccount[account_]
749+ } else {
750+ var settings = favoriteByAccountComponent.createObject(this, {'category': account_})
751+ _favoritesByAccount[account_] = settings
752+ return settings
753+ }
754+ }
755+
756+ function favoriteIndexFromSettings(settings, fav)
757+ {
758+ return settings.favoriteChannels.split(";").indexOf(fav)
759+ }
760+
761+ function favoriteChannels(settings)
762+ {
763+ if (settings.favoriteChannels.length > 0) {
764+ return settings.favoriteChannels.split(";")
765+ } else {
766+ return []
767+ }
768+ }
769+
770+ Component {
771+ id: favoriteByAccountComponent
772+ Settings {
773+ objectName: "settings_" + category
774+ property string favoriteChannels: ""
775+ }
776+ }
777+}
778
779=== modified file 'src/qml/GroupChatInfoPage.qml'
780--- src/qml/GroupChatInfoPage.qml 2016-10-28 16:22:15 +0000
781+++ src/qml/GroupChatInfoPage.qml 2017-03-27 19:24:23 +0000
782@@ -56,9 +56,19 @@
783 }
784 return []
785 }
786+
787+ ParticipantsModel {
788+ id: participantsModel
789+ chatEntry: groupChatInfoPage.chatEntry.active ? groupChatInfoPage.chatEntry : null
790+ }
791+
792+ property var participantsSize: participants.length + localPendingParticipants.length + remotePendingParticipants.length
793+
794 property variant allParticipants: {
795 var participantList = []
796-
797+ if (chatEntry.active) {
798+ return participantList
799+ }
800 for (var i in participants) {
801 var participant = participants[i]
802 participant["state"] = 0
803@@ -84,6 +94,7 @@
804 participantList.push(participant)
805 }
806 }
807+ participantList.sort(function(a,b) {return (a.identifier.toLowerCase() > b.identifier.toLowerCase()) ? 1 : ((b.identifier.toLowerCase() > a.identifier.toLowerCase()) ? -1 : 0);} );
808 return participantList
809 }
810
811@@ -95,6 +106,38 @@
812 property bool chatRoom: chatType == HistoryThreadModel.ChatTypeRoom
813 property var chatRoomInfo: threads.length > 0 ? threads[0].chatRoomInfo : []
814
815+ property var leaveString: {
816+ // FIXME: temporary workaround
817+ if (account && account.protocolInfo.name == "irc") {
818+ return i18n.tr("Leave channel")
819+ }
820+ return i18n.tr("Leave group")
821+ }
822+
823+ property var headerString: {
824+ // FIXME: temporary workaround
825+ if (account && account.protocolInfo.name == "irc") {
826+ return i18n.tr("Channel Info")
827+ }
828+ return i18n.tr("Group Info")
829+ }
830+
831+ property var leaveSuccessString: {
832+ // FIXME: temporary workaround
833+ if (account && account.protocolInfo.name == "irc") {
834+ return i18n.tr("Successfully left channel")
835+ }
836+ return i18n.tr("Successfully left group")
837+ }
838+
839+ property var leaveFailedString: {
840+ // FIXME: temporary workaround
841+ if (account && account.protocolInfo.name == "irc") {
842+ return i18n.tr("Failed to leave channel")
843+ }
844+ return i18n.tr("Failed to leave group")
845+ }
846+
847 // self contact isn't provided by history or chatEntry, so we manually add it here
848 Item {
849 id: selfContactWatcher
850@@ -127,10 +170,37 @@
851
852 header: PageHeader {
853 id: pageHeader
854- title: i18n.tr("Group Info")
855+ title: groupChatInfoPage.headerString
856 // FIXME: uncomment once the header supports subtitle
857 //subtitle: i18n.tr("%1 member", "%1 members", allParticipants.length)
858- flickable: contentsFlickable
859+
860+ trailingActionBar {
861+ id: trailingBar
862+ actions: [
863+ Action {
864+ iconName: "close"
865+ text: i18n.tr("End group")
866+ onTriggered: destroyGroup()
867+ enabled: chatRoom && !isPhoneAccount && chatEntry.active && chatEntry.selfContactRoles & 2
868+ visible: enabled
869+ },
870+ Action {
871+ iconName: "system-log-out"
872+ text: groupChatInfoPage.leaveString
873+ visible: enabled
874+ onTriggered: {
875+ if (chatEntry.leaveChat()) {
876+ application.showNotificationMessage(groupChatInfoPage.leaveSuccessString, "tick")
877+ mainView.emptyStack()
878+ } else {
879+ application.showNotificationMessage(groupChatInfoPage.leaveFailedString, "dialog-error-symbolic")
880+ }
881+
882+ }
883+ enabled: chatRoom && !isPhoneAccount && chatEntry.active && !(chatEntry.selfContactRoles & 2)
884+ }
885+ ]
886+ }
887 }
888
889 function addRecipientFromSearch(identifier, alias, avatar) {
890@@ -138,12 +208,25 @@
891 }
892
893 function addRecipient(identifier, contact) {
894- for (var i=0; i < allParticipants; i++) {
895- if (identifier == allParticipants[i].identifier) {
896- application.showNotificationMessage(i18n.tr("This recipient was already selected"), "dialog-error-symbolic")
897- return
898- }
899- }
900+ for (var i=0; i < participants; i++) {
901+ if (identifier == participants[i].identifier) {
902+ application.showNotificationMessage(i18n.tr("This recipient was already selected"), "dialog-error-symbolic")
903+ return
904+ }
905+ }
906+ for (var i=0; i < localPendingParticipants; i++) {
907+ if (identifier == localPendingParticipants[i].identifier) {
908+ application.showNotificationMessage(i18n.tr("This recipient was already selected"), "dialog-error-symbolic")
909+ return
910+ }
911+ }
912+ for (var i=0; i < remotePendingParticipants; i++) {
913+ if (identifier == remotePendingParticipants[i].identifier) {
914+ application.showNotificationMessage(i18n.tr("This recipient was already selected"), "dialog-error-symbolic")
915+ return
916+ }
917+ }
918+
919 searchItem.text = ""
920
921 chatEntry.inviteParticipants([identifier], "")
922@@ -167,30 +250,22 @@
923 }
924 }
925
926- Flickable {
927+ ListView {
928 id: contentsFlickable
929- property var emptySpaceHeight: height - contentsColumn.topItemsHeight+contentsFlickable.contentY
930 anchors {
931- top: parent.top
932+ top: groupChatInfoPage.header.top
933+ topMargin: groupChatInfoPage.header.height
934 left: parent.left
935 right: parent.right
936 bottom: keyboard.top
937 }
938- contentHeight: contentsColumn.height
939- clip: true
940-
941- Column {
942- id: contentsColumn
943- property var topItemsHeight: groupInfo.height+participantsHeader.height+searchItem.height+units.gu(1)
944-
945+
946+ header: Item {
947 anchors {
948- top: parent.top
949 left: parent.left
950 right: parent.right
951 }
952-
953 height: childrenRect.height
954-
955 Item {
956 id: groupInfo
957 height: visible ? groupAvatar.height + groupAvatar.anchors.topMargin + units.gu(1) : 0
958@@ -198,6 +273,7 @@
959 enabled: chatEntry.active
960
961 anchors {
962+ top: parent.top
963 left: parent.left
964 right: parent.right
965 }
966@@ -278,18 +354,11 @@
967 }
968 }
969
970- ListItems.ThinDivider {
971- visible: groupInfo.visible
972- anchors {
973- left: parent.left
974- right: parent.right
975- }
976- }
977-
978 Item {
979 id: participantsHeader
980 enabled: chatEntry.active
981 anchors {
982+ top: groupInfo.bottom
983 left: parent.left
984 right: parent.right
985 }
986@@ -302,7 +371,7 @@
987 leftMargin: units.gu(2)
988 verticalCenter: addParticipantButton.verticalCenter
989 }
990- text: !searchItem.enabled ? i18n.tr("Participants: %1").arg(allParticipants.length) : i18n.tr("Add participant:")
991+ text: !searchItem.enabled ? i18n.tr("Participants: %1").arg(participantsSize) : i18n.tr("Add participant:")
992 }
993
994 Button {
995@@ -318,6 +387,10 @@
996 if (!chatRoom || !chatEntry.active) {
997 return false
998 }
999+ // FIXME: temporary workaround
1000+ if (account && account.protocolInfo.name == "irc") {
1001+ return false
1002+ }
1003 return (chatEntry.groupFlags & ChatEntry.ChannelGroupFlagCanAdd)
1004 }
1005 text: !searchItem.enabled ? i18n.tr("Add...") : i18n.tr("Cancel")
1006@@ -328,22 +401,16 @@
1007 }
1008 }
1009
1010- ListItems.ThinDivider {
1011- anchors {
1012- left: parent.left
1013- right: parent.right
1014- }
1015- }
1016-
1017 ContactSearchWidget {
1018 id: searchItem
1019 enabled: false
1020 height: enabled ? units.gu(6) : 0
1021 clip: true
1022 parentPage: groupChatInfoPage
1023- searchResultsHeight: contentsFlickable.emptySpaceHeight
1024+ searchResultsHeight: keyboard.y-y-height
1025 onContactPicked: addRecipientFromSearch(identifier, alias, avatar)
1026 anchors {
1027+ top: participantsHeader.bottom
1028 left: parent.left
1029 right: parent.right
1030 }
1031@@ -351,120 +418,86 @@
1032 UbuntuNumberAnimation {}
1033 }
1034 }
1035+ }
1036
1037- ListItems.ThinDivider {
1038- visible: searchItem.enabled
1039- anchors {
1040- left: parent.left
1041- right: parent.right
1042- }
1043+ ListItemActions {
1044+ id: participantLeadingActions
1045+ delegate: Label {
1046+ anchors.verticalCenter: parent.verticalCenter
1047+ anchors.horizontalCenter: parent.horizontalCenter
1048+ height: contentHeight
1049+ width: contentWidth+units.gu(2)
1050+ verticalAlignment: Text.AlignVCenter
1051+ horizontalAlignment: Text.AlignHCenter
1052+ text: i18n.tr("Remove")
1053 }
1054-
1055-
1056- ListItemActions {
1057- id: participantLeadingActions
1058- delegate: Label {
1059- anchors.verticalCenter: parent.verticalCenter
1060- anchors.horizontalCenter: parent.horizontalCenter
1061- height: contentHeight
1062- width: contentWidth+units.gu(2)
1063- verticalAlignment: Text.AlignVCenter
1064- horizontalAlignment: Text.AlignHCenter
1065+ actions: [
1066+ Action {
1067 text: i18n.tr("Remove")
1068- }
1069- actions: [
1070- Action {
1071- text: i18n.tr("Remove")
1072- onTriggered: {
1073- // in case account is not a phone one, alert that if the group is going to have no active participants
1074- // it can be dissolved by the server
1075- if (chatEntry.chatType == ChatEntry.ChatTypeRoom && chatEntry.participants.length === 1 /*the active participant to remove now*/) {
1076- var properties = {}
1077- properties["groupName"] = groupName.text
1078- PopupUtils.open(Qt.createComponent("Dialogs/EmptyGroupWarningDialog.qml").createObject(groupChatInfoPage), groupChatInfoPage, properties)
1079- } else {
1080- var delegate = participantsRepeater.itemAt(value)
1081- delegate.removeFromGroup();
1082- }
1083- }
1084- }
1085- ]
1086- }
1087-
1088- Repeater {
1089- id: participantsRepeater
1090- model: allParticipants
1091-
1092- ParticipantDelegate {
1093- id: participantDelegate
1094- function canRemove() {
1095- if (!groupChatInfoPage.chatRoom /*not a group*/
1096- || !chatEntry.active /*not active*/
1097- || modelData.roles & 2 /*not admin*/
1098- || modelData.state === 2 /*remote pending*/) {
1099- return false
1100- }
1101- return (chatEntry.groupFlags & ChatEntry.ChannelGroupFlagCanRemove)
1102- }
1103- function removeFromGroup() {
1104- var participant = participantDelegate.participant
1105- chatEntry.removeParticipants([participant.identifier], "")
1106- participantDelegate.height = 0
1107- }
1108- participant: modelData
1109- leadingActions: canRemove() ? participantLeadingActions : undefined
1110- onClicked: {
1111- if (openProfileButton.visible) {
1112- mainStack.addPageToCurrentColumn(groupChatInfoPage, Qt.resolvedUrl("ParticipantInfoPage.qml"), {"delegate": participantDelegate, "chatEntry": chatEntry, "chatRoom": chatRoom})
1113- }
1114- }
1115- Icon {
1116- id: openProfileButton
1117- anchors.right: parent.right
1118- anchors.rightMargin: units.gu(1)
1119- anchors.verticalCenter: parent.verticalCenter
1120- height: units.gu(2)
1121- name: "go-next"
1122- }
1123- }
1124- }
1125- Item {
1126- id: padding
1127- height: units.gu(3)
1128- anchors.left: parent.left
1129+ onTriggered: {
1130+ // in case account is not a phone one, alert that if the group is going to have no active participants
1131+ // it can be dissolved by the server
1132+ if (chatEntry.chatType == ChatEntry.ChatTypeRoom && chatEntry.participants.length === 1 /*the active participant to remove now*/) {
1133+ var properties = {}
1134+ properties["groupName"] = groupName.text
1135+ PopupUtils.open(Qt.createComponent("Dialogs/EmptyGroupWarningDialog.qml").createObject(groupChatInfoPage), groupChatInfoPage, properties)
1136+ } else {
1137+ var delegate = contentsFlickable.itemAt(value)
1138+ delegate.removeFromGroup();
1139+ }
1140+ }
1141+ }
1142+ ]
1143+ }
1144+
1145+ model: chatEntry.active ? participantsModel : allParticipants
1146+
1147+ delegate: ParticipantDelegate {
1148+ id: participantDelegate
1149+ function canRemove() {
1150+ if (!groupChatInfoPage.chatRoom /*not a group*/
1151+ || !chatEntry.active /*not active*/
1152+ || model.roles & 2 /*not admin*/
1153+ || model.state === 2 /*remote pending*/) {
1154+ return false
1155+ }
1156+ // FIXME: temporary workaround
1157+ if (account && account.protocolInfo.name == "irc") {
1158+ return false
1159+ }
1160+ return (chatEntry.groupFlags & ChatEntry.ChannelGroupFlagCanRemove)
1161+ }
1162+ function removeFromGroup() {
1163+ var participant = participantDelegate.participant
1164+ chatEntry.removeParticipants([participant.identifier], "")
1165+ participantDelegate.height = 0
1166+ }
1167+ participant: chatEntry.active ? model : modelData
1168+ leadingActions: canRemove() ? participantLeadingActions : null
1169+ onClicked: {
1170+ if (openProfileButton.visible) {
1171+ mainStack.addPageToCurrentColumn(groupChatInfoPage,
1172+ Qt.resolvedUrl("ParticipantInfoPage.qml"),
1173+ {"delegate": participantDelegate,
1174+ "chatEntry": chatEntry,
1175+ "chatRoom": chatRoom,
1176+ "protocolName": account.protocolInfo.name })
1177+ }
1178+ }
1179+ Icon {
1180+ id: openProfileButton
1181 anchors.right: parent.right
1182- }
1183- Row {
1184- enabled: chatEntry.active
1185- anchors {
1186- right: parent.right
1187- rightMargin: units.gu(2)
1188- }
1189- layoutDirection: Qt.RightToLeft
1190- spacing: units.gu(1)
1191- Button {
1192- id: destroyButton
1193- visible: chatRoom && !isPhoneAccount && chatEntry.active && chatEntry.selfContactRoles & 2
1194- text: i18n.tr("End group")
1195- color: Theme.palette.normal.negative
1196- onClicked: destroyGroup()
1197- }
1198- Button {
1199- id: leaveButton
1200- visible: chatRoom && !isPhoneAccount && chatEntry.active && !(chatEntry.selfContactRoles & 2)
1201- text: i18n.tr("Leave group")
1202- onClicked: {
1203- if (chatEntry.leaveChat()) {
1204- application.showNotificationMessage(i18n.tr("Successfully left group"), "tick")
1205- mainView.emptyStack()
1206- } else {
1207- application.showNotificationMessage(i18n.tr("Failed to leave group"), "dialog-error-symbolic")
1208- }
1209- }
1210- }
1211+ anchors.rightMargin: units.gu(1)
1212+ anchors.verticalCenter: parent.verticalCenter
1213+ height: units.gu(2)
1214+ name: "go-next"
1215 }
1216 }
1217 }
1218+ Scrollbar {
1219+ flickableItem: contentsFlickable
1220+ align: Qt.AlignTrailing
1221+ }
1222 KeyboardRectangle {
1223 id: keyboard
1224 }
1225
1226=== modified file 'src/qml/MainPage.qml'
1227--- src/qml/MainPage.qml 2016-11-09 10:33:23 +0000
1228+++ src/qml/MainPage.qml 2017-03-27 19:24:23 +0000
1229@@ -32,13 +32,18 @@
1230 property bool isEmpty: threadCount == 0 && !threadModel.canFetchMore
1231 property alias threadCount: threadList.count
1232 property alias displayedThreadIndex: threadList.currentIndex
1233-
1234- property var _messagesPage: null
1235+ property bool _keepFocus: true
1236
1237 function startSelection() {
1238 threadList.startSelection()
1239 }
1240
1241+ function selectMessage(index) {
1242+ if (index !== -1)
1243+ _keepFocus = false
1244+ threadList.currentIndex = index
1245+ }
1246+
1247 signal newThreadCreated(var newThread)
1248
1249 TextField {
1250@@ -91,6 +96,8 @@
1251 objectName: "searchAction"
1252 iconName: "search"
1253 text: i18n.tr("Search")
1254+ shortcut: "Ctrl+F"
1255+ enabled: mainPage.state == "default"
1256 onTriggered: {
1257 mainPage.searching = true
1258 searchField.forceActiveFocus()
1259@@ -101,6 +108,7 @@
1260 text: i18n.tr("Settings")
1261 iconName: "settings"
1262 onTriggered: {
1263+ threadList.currentIndex = -1
1264 pageStack.addPageToNextColumn(mainPage, Qt.resolvedUrl("SettingsPage.qml"))
1265 }
1266 },
1267@@ -108,7 +116,12 @@
1268 objectName: "newMessageAction"
1269 text: i18n.tr("New message")
1270 iconName: "add"
1271- onTriggered: mainView.startNewMessage()
1272+ shortcut: "Ctrl+N"
1273+ enabled: mainPage.state == "default"
1274+ onTriggered: {
1275+ threadList.currentIndex = -1
1276+ mainView.startNewMessage()
1277+ }
1278 }
1279 ]
1280
1281@@ -129,6 +142,8 @@
1282 visible: mainPage.searching
1283 iconName: "back"
1284 text: i18n.tr("Cancel")
1285+ shortcut: "Esc"
1286+ enabled: mainPage.state == "search"
1287 onTriggered: {
1288 searchField.text = ""
1289 mainPage.searching = false
1290@@ -152,7 +167,9 @@
1291 Action {
1292 objectName: "selectionModeCancelAction"
1293 iconName: "back"
1294+ shortcut: "Esc"
1295 onTriggered: threadList.cancelSelection()
1296+ enabled: mainPage.state == "selection"
1297 }
1298 ]
1299
1300@@ -192,6 +209,20 @@
1301 Component {
1302 id: sectionDelegate
1303 ThreadsSectionDelegate {
1304+ function formatSectionTitle(title) {
1305+ if (mainView.sortThreadsBy === "timestamp")
1306+ return DateUtils.friendlyDay(Qt.formatDate(section, "yyyy/MM/dd"), i18n);
1307+ else if (telepathyHelper.ready) {
1308+ var account = telepathyHelper.accountForId(title)
1309+ if (account.connectionStatus == AccountEntry.ConnectionStatusConnecting) {
1310+ return i18n.tr("%1 - Connecting...").arg(account.displayName)
1311+ } else {
1312+ return account.displayName
1313+ }
1314+ }
1315+ else
1316+ return title
1317+ }
1318 }
1319 }
1320
1321@@ -211,9 +242,9 @@
1322 // but not completely revealed.
1323 enabled: bottomEdgeLoader.item.status !== BottomEdge.Revealed
1324 clip: true
1325- section.property: "eventDate"
1326 currentIndex: -1
1327 //spacing: searchField.text === "" ? units.gu(-2) : 0
1328+ section.property: mainView.sortThreadsBy === "title" ? "accountId" : "eventDate"
1329 section.delegate: searching && searchField.text !== "" ? null : sectionDelegate
1330 header: ListItem.Standard {
1331 // FIXME: update
1332@@ -225,17 +256,46 @@
1333 selected: true
1334 }
1335
1336- listDelegate: ThreadDelegate {
1337+ onCurrentItemChanged: {
1338+ if (pageStack.columns > 1) {
1339+ currentItem.show()
1340+ if (mainPage._keepFocus)
1341+ // Keep focus on current page
1342+ threadList.forceActiveFocus()
1343+ else if (pageStack.activePage)
1344+ pageStack.activePage.forceActiveFocus()
1345+ mainPage._keepFocus = true
1346+ }
1347+ }
1348+
1349+ listDelegate: ThreadDelegate {
1350 id: threadDelegate
1351+
1352+ function show()
1353+ {
1354+ var properties = model.properties
1355+ properties["keyboardFocus"] = false
1356+ properties["threads"] = model.threads
1357+ properties["presenceRequest"] = threadDelegate.presenceItem
1358+ if (displayedEvent != null) {
1359+ properties["scrollToEventId"] = displayedEvent.eventId
1360+ }
1361+ properties["chatEntry"] = chatEntry
1362+ delete properties["participants"]
1363+ delete properties["localPendingParticipants"]
1364+ delete properties["remotePendingParticipants"]
1365+ mainView.showMessagesView(properties)
1366+ }
1367+
1368 // FIXME: find a better unique name
1369- objectName: "thread%1".arg(participants[0].identifier)
1370+ objectName: "thread%1".arg(participants.length > 0 ? participants[0].identifier : "")
1371 Component.onCompleted: mainPage.newThreadCreated(model)
1372
1373 anchors {
1374 left: parent.left
1375 right: parent.right
1376 }
1377- height: units.gu(8)
1378+ compactView: mainView.compactView
1379 selectMode: threadList.isInSelectionMode
1380 selected: {
1381 if (selectMode) {
1382@@ -245,38 +305,34 @@
1383 }
1384
1385 searchTerm: mainPage.searching ? searchField.text : ""
1386+
1387 onClicked: {
1388 if (threadList.isInSelectionMode) {
1389 if (!threadList.selectItem(threadDelegate)) {
1390 threadList.deselectItem(threadDelegate)
1391 }
1392- } else {
1393- var properties = model.properties
1394-
1395- properties["keyboardFocus"] = false
1396- properties["threads"] = model.threads
1397- var participantIds = [];
1398- for (var i in model.participants) {
1399- participantIds.push(model.participants[i].identifier)
1400- }
1401- properties["participantIds"] = participantIds
1402- properties["presenceRequest"] = threadDelegate.presenceItem
1403- if (displayedEvent != null) {
1404- properties["scrollToEventId"] = displayedEvent.eventId
1405- }
1406- delete properties["participants"]
1407- delete properties["localPendingParticipants"]
1408- delete properties["remotePendingParticipants"]
1409- mainView.showMessagesView(properties)
1410+ }
1411+ threadList.currentIndex = index
1412
1413- // mark this item as current
1414- threadList.currentIndex = index
1415+ if (pageStack.columns <= 1) {
1416+ show()
1417 }
1418 }
1419 onPressAndHold: {
1420 threadList.startSelection()
1421 threadList.selectItem(threadDelegate)
1422 }
1423+
1424+ ChatEntry {
1425+ id: chatEntry
1426+ chatType: model.properties.chatType
1427+ participantIds: model.properties.participantIds ? model.properties.participantIds : []
1428+ chatId: model.properties.threadId
1429+ accountId: model.properties.accountId
1430+ autoRequest: false
1431+ }
1432+
1433+ opacity: !groupChat || chatEntry.active ? 1.0 : 0.5
1434 }
1435 onSelectionDone: {
1436 var threadsToRemove = []
1437@@ -312,7 +368,7 @@
1438 incubator = component.incubateObject(parent, properties, Qt.Asynchronous);
1439
1440 function objectCreated(status) {
1441- if (status == Component.Ready && callback != null) {
1442+ if (status == Component.Ready && callback != undefined && callback != null) {
1443 callback(incubator.object);
1444 }
1445 }
1446@@ -330,9 +386,12 @@
1447 interval: 1
1448 repeat: false
1449 running: true
1450- onTriggered: createQmlObjectAsynchronously(Qt.resolvedUrl("Scrollbar.qml"),
1451- mainPage,
1452- {"flickableItem": threadList})
1453+ onTriggered: {
1454+ createQmlObjectAsynchronously(Qt.resolvedUrl("Scrollbar.qml"),
1455+ mainPage,
1456+ {"flickableItem": threadList})
1457+ threadList.forceActiveFocus()
1458+ }
1459 }
1460
1461 Loader {
1462@@ -348,4 +407,19 @@
1463 hint.visible: enabled
1464 }
1465 }
1466+
1467+ onActiveFocusChanged: {
1468+ if (activeFocus) {
1469+ threadList.currentItem.forceActiveFocus()
1470+ }
1471+ }
1472+
1473+ Binding {
1474+ target: pageStack
1475+ property: "activePage"
1476+ value: mainPage
1477+ when: pageStack.columns === 1
1478+ }
1479+
1480+ KeyNavigation.right: pageStack.activePage
1481 }
1482
1483=== modified file 'src/qml/MessageBubble.qml'
1484--- src/qml/MessageBubble.qml 2016-10-21 12:22:44 +0000
1485+++ src/qml/MessageBubble.qml 2017-03-27 19:24:23 +0000
1486@@ -35,6 +35,8 @@
1487 property var messageTimeStamp
1488 property int maxDelegateWidth: units.gu(27)
1489 property string accountName
1490+ property var account
1491+ property var _accountRegex: account && (account.selfContactId != "") ? new RegExp('\\b' + account.selfContactId + '\\b', 'g') : null
1492 property bool isMultimedia: false
1493 // FIXME for now we just display the delivery status if it's greater than Accepted
1494 property bool showDeliveryStatus: false
1495@@ -77,6 +79,11 @@
1496 var currentNumber = phoneNumbers[i]
1497 text = text.replace(currentNumber, formatTelSchemeWith(currentNumber))
1498 }
1499+
1500+ // hightlight participants names
1501+ if (_accountRegex)
1502+ text = text.replace(_accountRegex, "<b>" + account.selfContactId + "</b>")
1503+
1504 return text
1505 }
1506
1507
1508=== modified file 'src/qml/MessageDelegate.qml'
1509--- src/qml/MessageDelegate.qml 2016-11-18 21:15:46 +0000
1510+++ src/qml/MessageDelegate.qml 2017-03-27 19:24:23 +0000
1511@@ -46,6 +46,8 @@
1512 property string accountLabel: ""
1513 property bool isMultimedia: false
1514 property var _lastItem: textBubble.visible ? textBubble : attachmentsLoader.item.lastItem
1515+ property alias account: textBubble.account
1516+
1517 swipeEnabled: !(attachmentsLoader.item && attachmentsLoader.item.swipeLocked)
1518
1519 function deleteMessage()
1520@@ -188,11 +190,14 @@
1521 }
1522 highlightColor: "transparent"
1523
1524- onClicked: {
1525- if (!selectMode) {
1526- // we only have actions for attachment items, so forward the click
1527- if (attachmentsLoader.item) {
1528- attachmentsLoader.item.clicked(mouse)
1529+ MouseArea {
1530+ anchors.fill: parent
1531+ onClicked: {
1532+ if (!selectMode) {
1533+ // we only have actions for attachment items, so forward the click
1534+ if (attachmentsLoader.item) {
1535+ attachmentsLoader.item.clicked(mouse)
1536+ }
1537 }
1538 }
1539 }
1540@@ -245,6 +250,7 @@
1541
1542 MessageBubble {
1543 id: textBubble
1544+
1545 isMultimedia: messageDelegate.isMultimedia
1546 anchors {
1547 bottom: parent.bottom
1548@@ -283,7 +289,20 @@
1549 messageTimeStamp: messageData.timestamp
1550 accountName: messageDelegate.accountLabel
1551 messageStatus: messageData.textMessageStatus
1552- sender: (messages.chatType == HistoryThreadModel.ChatTypeRoom || messageData.participants.length > 1) ? messageData.sender.alias !== "" ? messageData.sender.alias : messageData.senderId : ""
1553+ sender: {
1554+ if (messages.chatType == HistoryThreadModel.ChatTypeRoom || messageData.participants.length > 1) {
1555+ if (messageData.sender && messageIncoming) {
1556+ if (messageData.sender.alias !== undefined && messageData.sender.alias !== "") {
1557+ return messageData.sender.alias
1558+ } else if (messageData.sender.identifier !== undefined && messageData.sender.identifier !== "") {
1559+ return messageData.sender.identifier
1560+ } else if (messageData.senderId !== "") {
1561+ return messageData.senderId
1562+ }
1563+ }
1564+ }
1565+ return ""
1566+ }
1567 showDeliveryStatus: true
1568 }
1569
1570
1571=== modified file 'src/qml/MessageInfoDialog.qml'
1572--- src/qml/MessageInfoDialog.qml 2015-11-03 13:16:43 +0000
1573+++ src/qml/MessageInfoDialog.qml 2017-03-27 19:24:23 +0000
1574@@ -78,61 +78,96 @@
1575
1576 function getTargetName(message)
1577 {
1578+ if (!message)
1579+ return ""
1580+
1581 if (message.senderId !== "self") {
1582 return i18n.tr("Myself")
1583- } else if (message.participants.length > 1) {
1584+ } else if (message.participants && (message.participants.length > 1)) {
1585 return i18n.tr("Group")
1586+ } else if (message.participants.length > 0) {
1587+ return message.participants[0].identifier
1588 } else {
1589- return PhoneUtils.PhoneUtils.format(message.participants[0].identifier)
1590+ return i18n.tr("Unknown")
1591 }
1592 }
1593
1594 title: i18n.tr("Message info")
1595
1596 Label {
1597- text: "<b>%1:</b> %2".arg(i18n.tr("Type")).arg(root.activeMessage.type)
1598+ text: root.activeMessage ? "<b>%1:</b> %2".arg(i18n.tr("Type")).arg(root.activeMessage.type) : ""
1599 }
1600
1601 Label {
1602 text: "<b>%1:</b> %2".arg(i18n.tr("From"))
1603- .arg(root.activeMessage.senderId !== "self" ?
1604- PhoneUtils.PhoneUtils.format(root.activeMessage.senderId) : i18n.tr("Myself"))
1605+ .arg(root.activeMessage && root.activeMessage.senderId !== "self" ?
1606+ root.activeMessage && root.activeMessage.senderId : i18n.tr("Myself"))
1607 }
1608
1609 Label {
1610 text: "<b>%1:</b> %2".arg(i18n.tr("To"))
1611 .arg(getTargetName(root.activeMessage))
1612 }
1613- Repeater {
1614- model: root.activeMessage.senderId === "self" && root.activeMessage.participants.length > 1 ? root.activeMessage.participants : []
1615- Label {
1616- text: PhoneUtils.PhoneUtils.format(modelData.identifier)
1617- }
1618- }
1619-
1620- Label {
1621- text: "<b>%1:</b> %2".arg(i18n.tr("Sent")).arg(Qt.formatDateTime(root.activeMessage.timestamp, Qt.DefaultLocaleShortDate))
1622- visible: (root.activeMessage.senderId === "self")
1623- }
1624-
1625- Label {
1626- text: "<b>%1:</b> %2".arg(i18n.tr("Received")).arg(Qt.formatDateTime(root.activeMessage.timestamp, Qt.DefaultLocaleShortDate))
1627- visible: (root.activeMessage.senderId !== "self")
1628- }
1629-
1630- Label {
1631- text: "<b>%1:</b> %2".arg(i18n.tr("Read")).arg(Qt.formatDateTime(root.activeMessage.textReadTimestamp, Qt.DefaultLocaleShortDate))
1632- visible: (root.activeMessage.senderId !== "self") && (root.activeMessage.textReadTimestamp > 0)
1633- }
1634-
1635- Label {
1636- text: "<b>%1:</b> %2".arg(i18n.tr("Status")).arg(statusToString(root.activeMessage.status))
1637+
1638+ /*
1639+ // Disable list of contacts for now, this is not reliable on a IRC channel for example
1640+ // the current participants can not the same at the moment when the message was sent
1641+ ListView {
1642+ anchors {
1643+ left: parent.left
1644+ right: parent.right
1645+ }
1646+
1647+ height: units.gu(10) //Math.min(count * units.gu(3), units.gu(3))
1648+ model: root.activeMessage && root.activeMessage.senderId === "self" && root.activeMessage.participants.length > 1 ? root.activeMessage.participants : []
1649+ delegate: ListItem {
1650+ height: itemLayout.height + (divider.visible ? divider.height : 0)
1651+
1652+ ListItemLayout {
1653+ id: itemLayout
1654+
1655+ title.text: {
1656+ var formatted = PhoneUtils.PhoneUtils.format(modelData.identifier)
1657+ if (formatted.length > 0)
1658+ return formatted
1659+ else
1660+ return modelData.identifier
1661+ }
1662+ }
1663+ }
1664+ }
1665+ */
1666+
1667+ Label {
1668+ text: root.activeMessage ?
1669+ "<b>%1:</b> %2".arg(i18n.tr("Sent")).arg(Qt.formatDateTime(root.activeMessage.timestamp, Qt.DefaultLocaleShortDate)) :
1670+ ""
1671+ visible: root.activeMessage && (root.activeMessage.senderId === "self")
1672+ }
1673+
1674+ Label {
1675+ text: root.activeMessage ?
1676+ "<b>%1:</b> %2".arg(i18n.tr("Received")).arg(Qt.formatDateTime(root.activeMessage.timestamp, Qt.DefaultLocaleShortDate)) :
1677+ ""
1678+ visible: (root.activeMessage && root.activeMessage.senderId !== "self")
1679+ }
1680+
1681+ Label {
1682+ text: root.activeMessage ?
1683+ "<b>%1:</b> %2".arg(i18n.tr("Read")).arg(Qt.formatDateTime(root.activeMessage.textReadTimestamp, Qt.DefaultLocaleShortDate)) :
1684+ ""
1685+ visible: root.activeMessage && (root.activeMessage.senderId !== "self") && (root.activeMessage.textReadTimestamp > 0)
1686+ }
1687+
1688+ Label {
1689+ text: root.activeMessage ? "<b>%1:</b> %2".arg(i18n.tr("Status")).arg(statusToString(root.activeMessage.status)) : ""
1690 }
1691
1692 Button {
1693- text: i18n.tr("Close")
1694- onClicked: {
1695- PopupUtils.close(root.activeDialog)
1696+ action: Action {
1697+ text: i18n.tr("Close")
1698+ shortcut: "esc"
1699+ onTriggered: PopupUtils.close(root.activeDialog)
1700 }
1701 }
1702
1703
1704=== modified file 'src/qml/Messages.qml'
1705--- src/qml/Messages.qml 2016-11-22 17:32:53 +0000
1706+++ src/qml/Messages.qml 2017-03-27 19:24:23 +0000
1707@@ -83,19 +83,29 @@
1708 property string scrollToEventId: ""
1709 property bool isSearching: scrollToEventId !== ""
1710 property string latestEventId: ""
1711- property var pendingEventsToMarkAsRead: []
1712 property bool reloadFilters: false
1713 // to be used by tests as variant does not work with autopilot
1714 property bool userTyping: false
1715 property string userTypingId: ""
1716 property string firstParticipantId: participantIds.length > 0 ? participantIds[0] : ""
1717- property variant firstParticipant: participants.length > 0 ? participants[0] : null
1718+ property variant firstParticipant: {
1719+ if (!participants || participants.length == 0) {
1720+ return null
1721+ }
1722+ var participant = participants[0]
1723+ if (typeof participant === "string") {
1724+ return {identifier: participant, alias: participant}
1725+ } else {
1726+ return participant
1727+ }
1728+ }
1729+
1730 property var threads: []
1731 property QtObject presenceRequest: presenceItem
1732 property var accountsModel: getAccountsModel()
1733 property alias oskEnabled: keyboard.oskEnabled
1734 property bool isReady: false
1735- property QtObject chatEntry: chatEntryObject
1736+ property QtObject chatEntry
1737 property string firstRecipientAlias: ((contactWatcher.isUnknown &&
1738 contactWatcher.isInteractive) ||
1739 contactWatcher.alias === "") ? contactWatcher.identifier : contactWatcher.alias
1740@@ -105,6 +115,20 @@
1741 property bool isBroadcast: chatType != ChatEntry.ChatTypeRoom && (participantIds.length > 1 || multiRecipient.recipientCount > 1)
1742
1743 property alias validator: sendMessageValidator
1744+ property string chatTitle: {
1745+ if (chatEntry.title !== "") {
1746+ return chatEntry.title
1747+ }
1748+ var roomInfo = threadInformation.chatRoomInfo
1749+ if (roomInfo) {
1750+ if (roomInfo.Title != "") {
1751+ return roomInfo.Title
1752+ } else if (roomInfo.RoomName != "") {
1753+ return roomInfo.RoomName
1754+ }
1755+ }
1756+ return ""
1757+ }
1758
1759 signal ready
1760 signal cancel
1761@@ -135,6 +159,9 @@
1762 for (var i in messages.accountsModel) {
1763 accountNames.push(messages.accountsModel[i].displayName)
1764 }
1765+ if (messages.accountsModel.length == 1 && messages.accountsModel[0].type == AccountEntry.GenericAccount) {
1766+ return accountNames
1767+ }
1768 return accountNames.length > 1 ? accountNames : []
1769 }
1770
1771@@ -189,9 +216,10 @@
1772 }
1773 }
1774 return null
1775- } else {
1776- return mainView.account
1777+ } else if (!(telepathyHelper.phoneAccounts.active.length > 0) && messages.accountsModel.length > 0) {
1778+ return messages.accountsModel[0]
1779 }
1780+ return mainView.account
1781 }
1782
1783 function checkThreadInFilters(newAccountId, threadId) {
1784@@ -457,6 +485,7 @@
1785 }
1786
1787 function updateFilters(accounts, chatType, participantIds, reload, threads) {
1788+ selectThreadOnIdle.restart()
1789 if (participantIds.length == 0 || accounts.length == 0) {
1790 if (chatType != HistoryThreadModel.ChatTypeRoom) {
1791 return null
1792@@ -509,38 +538,76 @@
1793 return Qt.createQmlObject(componentUnion.arg(componentFilters), eventModel)
1794 }
1795
1796- function markMessageAsRead(accountId, threadId, eventId, type) {
1797- var pendingEvent = {"accountId": accountId, "threadId": threadId, "messageId": eventId, "type": type, "chatType": messages.chatType, 'participantIds': messages.participantIds}
1798- if (!mainView.applicationActive || !messages.active) {
1799- pendingEventsToMarkAsRead.push(pendingEvent)
1800- return false
1801- }
1802- chatManager.acknowledgeMessage(pendingEvent)
1803- return eventModel.markEventAsRead(accountId, threadId, eventId, type);
1804- }
1805-
1806- function processPendingEvents() {
1807- if (mainView.applicationActive && messages.active) {
1808- for (var i in pendingEventsToMarkAsRead) {
1809- var event = pendingEventsToMarkAsRead[i]
1810- markMessageAsRead(event.accountId, event.threadId, event.messageId, event.type)
1811- }
1812- pendingEventsToMarkAsRead = []
1813- }
1814- }
1815+ function markThreadAsRead() {
1816+ if (!mainView.applicationActive || !messages.active || !messages.threads || messages.threads.length == 0) {
1817+ return
1818+ }
1819+
1820+ threadModel.markThreadsAsRead(messages.threads);
1821+ var properties = {'accountId': threads[0].accountId, 'threadId': threads[0].threadId, 'chatType': threads[0].chatType}
1822+ chatManager.acknowledgeAllMessages(properties)
1823+ }
1824+
1825+ function selectActiveThread(threads) {
1826+ if ((messages.chatType == HistoryEventModel.ChatTypeContact) &&
1827+ (messages.threads.length > 0)) {
1828+ var index = threadModel.indexOf(messages.threads[0].threadId, messages.threads[0].accountId)
1829+ if (index != -1) {
1830+ mainPage.selectMessage(index)
1831+ }
1832+ }
1833+ }
1834+
1835+ function participantIdentifierByProtocol(account, baseIdentifier) {
1836+ if (account && account.protocolInfo) {
1837+ switch(account.protocolInfo.name) {
1838+ case "irc":
1839+ if (account.parameters.server != "")
1840+ return "%1@%2".arg(baseIdentifier).arg(account.parameters.server)
1841+ return baseIdentifier
1842+ default:
1843+ return baseIdentifier
1844+ }
1845+ }
1846+ }
1847+
1848+ function contactMatchFieldFromProtocol(protocol, fallback) {
1849+ switch(protocol) {
1850+ case "irc":
1851+ return ["X-IRC"];
1852+ default:
1853+ return fallback
1854+ }
1855+ }
1856+
1857+ // Use a timer to make sure that 'threads' are correct set before try to select it
1858+ Timer {
1859+ id: selectThreadOnIdle
1860+ interval: 100
1861+ repeat: false
1862+ running: false
1863+ onTriggered: selectActiveThread(messages.threads)
1864+ }
1865+
1866
1867 header: PageHeader {
1868 id: pageHeader
1869
1870- property alias leadingActions: leadingBar.actions
1871+ property bool backEnabled: true
1872 property alias trailingActions: trailingBar.actions
1873+ property bool showSections: {
1874+ if (headerSections.model.length > 1) {
1875+ return true
1876+ }
1877+ return (messages.accountsModel.length == 1 && messages.accountsModel[0].type == AccountEntry.GenericAccount)
1878+ }
1879
1880 title: {
1881 if (landscape) {
1882 return ""
1883 }
1884
1885- if (participants.length == 1) {
1886+ if (participants && participants.length === 1) {
1887 return firstRecipientAlias
1888 }
1889
1890@@ -556,7 +623,7 @@
1891 leftMargin: units.gu(2)
1892 bottom: parent.bottom
1893 }
1894- visible: headerSections.model.length > 1
1895+ visible: pageHeader.showSections
1896 enabled: visible
1897 model: getSectionsModel()
1898 selectedIndex: getSelectedIndex()
1899@@ -569,11 +636,23 @@
1900 Component.onCompleted: model = getSectionsModel()
1901 }
1902
1903- extension: headerSections.model.length > 1 ? headerSections : null
1904+ extension: pageHeader.showSections ? headerSections : null
1905
1906- leadingActionBar {
1907- id: leadingBar
1908- }
1909+ leadingActionBar.actions: [
1910+ Action {
1911+ iconName: "back"
1912+ text: i18n.tr("Back")
1913+ shortcut: visible ? "Esc" : ""
1914+ visible: pageHeader.backEnabled
1915+ onTriggered: {
1916+ if (messages.state == "selection") {
1917+ messageList.cancelSelection()
1918+ } else {
1919+ mainView.emptyStack(true)
1920+ }
1921+ }
1922+ }
1923+ ]
1924
1925 trailingActionBar {
1926 id: trailingBar
1927@@ -584,6 +663,7 @@
1928 anchors {
1929 bottom: parent.bottom
1930 right: parent.right
1931+ bottomMargin: -headerSections.height
1932 }
1933 }
1934 }
1935@@ -594,14 +674,6 @@
1936 name: "selection"
1937 when: selectionMode
1938
1939- property list<QtObject> leadingActions: [
1940- Action {
1941- objectName: "selectionModeCancelAction"
1942- iconName: "back"
1943- onTriggered: messageList.cancelSelection()
1944- }
1945- ]
1946-
1947 property list<QtObject> trailingActions: [
1948 Action {
1949 objectName: "selectionModeSelectAllAction"
1950@@ -631,8 +703,8 @@
1951 PropertyChanges {
1952 target: pageHeader
1953 title: " "
1954- leadingActions: selectionState.leadingActions
1955 trailingActions: selectionState.trailingActions
1956+ backEnabled: true
1957 }
1958 },
1959 State {
1960@@ -645,25 +717,44 @@
1961 id: groupChatAction
1962 objectName: "groupChatAction"
1963 iconName: "contact-group"
1964- onTriggered: mainStack.addPageToCurrentColumn(messages, Qt.resolvedUrl("GroupChatInfoPage.qml"), { threadInformation: threadInformation, chatEntry: messages.chatEntry, eventModel: eventModel})
1965+ onTriggered: {
1966+ // at this point we are interested in the thread participants no matter what the channel type is
1967+ messagesModel.requestThreadParticipants(messages.threads)
1968+ mainStack.addPageToCurrentColumn(messages, Qt.resolvedUrl("GroupChatInfoPage.qml"), { threadInformation: threadInformation, chatEntry: messages.chatEntry, eventModel: eventModel})
1969+ }
1970+ },
1971+ Action {
1972+ id: rejoinGroupChatAction
1973+ objectName: "rejoinGroupChatAction"
1974+ enabled: !chatEntry.active && messages.account.protocolInfo.enableRejoin && messages.account.connected
1975+ visible: enabled
1976+ iconName: "view-refresh"
1977+ onTriggered: messages.chatEntry.startChat()
1978+ },
1979+ Action {
1980+ id: favoriteAction
1981+ visible: chatEntry.active && (messages.chatType == HistoryThreadModel.ChatTypeRoom)
1982+ iconName: mainView.favoriteChannels.isFavorite(messages.accountId, messages.chatTitle) ? "starred" : "non-starred"
1983+ onTriggered: {
1984+ if (iconName == "starred")
1985+ mainView.favoriteChannels.removeFavorite(messages.accountId, messages.chatTitle)
1986+ else
1987+ mainView.favoriteChannels.addFavorite(messages.accountId, messages.chatTitle)
1988+ }
1989 }
1990+
1991 ]
1992
1993 PropertyChanges {
1994 target: pageHeader
1995 // TRANSLATORS: %1 refers to the number of participants in a group chat
1996 title: {
1997- var finalParticipants = participants.length
1998+ var finalParticipants = (participants ? participants.length : 0)
1999 if (messages.chatType == HistoryThreadModel.ChatTypeRoom) {
2000- if (chatEntry.title !== "") {
2001- return chatEntry.title
2002- }
2003- var roomInfo = threadInformation.chatRoomInfo
2004- if (roomInfo.Title != "") {
2005- return roomInfo.Title
2006- } else if (roomInfo.RoomName != "") {
2007- return roomInfo.RoomName
2008- }
2009+ if (messages.chatTitle != "") {
2010+ return messages.chatTitle
2011+ }
2012+
2013 // include the "Me" participant to be consistent with
2014 // group info page
2015 if (roomInfo.Joined) {
2016@@ -674,17 +765,18 @@
2017 }
2018 contents: headerContents
2019 trailingActions: groupChatState.trailingActions
2020+ backEnabled: pageStack.columns === 1
2021 }
2022 },
2023 State {
2024 id: unknownContactState
2025 name: "unknownContact"
2026- when: participants.length == 1 && contactWatcher.isUnknown
2027+ when: !messages.newMessage && (participants.length === 1) && contactWatcher.isUnknown
2028
2029 property list<QtObject> trailingActions: [
2030 Action {
2031 objectName: "contactCallAction"
2032- visible: participants.length == 1 && contactWatcher.interactive
2033+ visible: participants && participants.length === 1 && contactWatcher.interactive && messages.account.addressableVCardFields.lastIndexOf("tel") != -1
2034 iconName: "call-start"
2035 text: i18n.tr("Call")
2036 onTriggered: {
2037@@ -695,13 +787,17 @@
2038 },
2039 Action {
2040 objectName: "addContactAction"
2041- visible: contactWatcher.isUnknown && participants.length == 1 && contactWatcher.interactive
2042+ visible: contactWatcher.isUnknown && participants && participants.length === 1 && contactWatcher.interactive
2043+ enabled: messages.account != null
2044 iconName: "contact-new"
2045 text: i18n.tr("Add")
2046 onTriggered: {
2047 Qt.inputMethod.hide()
2048- // FIXME: support other things than just phone numbers
2049- mainView.addPhoneToContact(messages, "", contactWatcher.identifier, null, null)
2050+ mainView.addAccountToContact(messages,
2051+ "",
2052+ messages.account.protocolInfo.name,
2053+ contactWatcher.identifier,
2054+ null, null)
2055 }
2056 }
2057 ]
2058@@ -709,6 +805,7 @@
2059 target: pageHeader
2060 contents: headerContents
2061 trailingActions: unknownContactState.trailingActions
2062+ backEnabled: pageStack.columns === 1
2063 }
2064 },
2065 State {
2066@@ -744,7 +841,7 @@
2067 mmsGroupAction.trigger()
2068 return
2069 }
2070- contextMenu.caller = header;
2071+ contextMenu.caller = trailingActionArea;
2072 contextMenu.updateGroupTypes();
2073 contextMenu.show();
2074 }
2075@@ -762,6 +859,12 @@
2076 top: parent ? parent.top: undefined
2077 topMargin: units.gu(1)
2078 }
2079+ onActiveFocusChanged: {
2080+ if (!activeFocus && (searchListLoader.status != Loader.Ready || !searchListLoader.item.activeFocus))
2081+ commit()
2082+ }
2083+
2084+ KeyNavigation.down: searchListLoader.item ? searchListLoader.item : composeBar.textArea
2085 }
2086
2087 PropertyChanges {
2088@@ -769,16 +872,18 @@
2089 title: " "
2090 trailingActions: newMessageState.trailingActions
2091 contents: newMessageState.contents
2092+ backEnabled: true
2093 }
2094 },
2095 State {
2096 id: knownContactState
2097 name: "knownContact"
2098- when: participants.length == 1 && !contactWatcher.isUnknown
2099+ when: !messages.newMessage && participants && participants.length === 1 && !contactWatcher.isUnknown
2100+
2101 property list<QtObject> trailingActions: [
2102 Action {
2103 objectName: "contactCallKnownAction"
2104- visible: participants.length == 1
2105+ visible: participants && participants.length === 1
2106 iconName: "call-start"
2107 text: i18n.tr("Call")
2108 onTriggered: {
2109@@ -801,11 +906,16 @@
2110 target: pageHeader
2111 contents: headerContents
2112 trailingActions: knownContactState.trailingActions
2113+ backEnabled: pageStack.columns === 1
2114 }
2115 }
2116 ]
2117
2118 Component.onCompleted: {
2119+ if (!chatEntry) {
2120+ chatEntry = chatEntryComponent.createObject(this)
2121+ }
2122+
2123 // we only revert back to phone account if this is a 1-1 chat,
2124 // in which case the handler will fallback to multimedia if needed
2125 if (messages.accountId !== "" && chatType !== HistoryThreadModel.ChatTypeRoom) {
2126@@ -820,14 +930,21 @@
2127 }
2128 }
2129 }
2130- newMessage = (messages.accountId == "" && messages.participants.length === 0)
2131 restoreBindings()
2132 if (threadId !== "" && accountId !== "" && threads.length == 0) {
2133 addNewThreadToFilter(accountId, {"threadId": threadId, "chatType": chatType})
2134 }
2135+ newMessage = (messages.threadId == "") || (messages.accountId == "" && messages.participants.length === 0)
2136+ // if it is a new message we need to add participants into the multiRecipient list
2137+ if (newMessage) {
2138+ for (var i in participantIds) {
2139+ multiRecipient.addRecipient(participantIds[i])
2140+ }
2141+ }
2142 // if we add multiple attachments at the same time, it break the Repeater + Loaders
2143 fillAttachmentsTimer.start()
2144 mainView.updateNewMessageStatus()
2145+ markThreadAsRead()
2146 }
2147
2148 Component.onDestruction: {
2149@@ -856,7 +973,7 @@
2150
2151 onReady: {
2152 isReady = true
2153- if (participants.length === 0 && keyboardFocus)
2154+ if (participants && participants.length === 0 && keyboardFocus)
2155 multiRecipient.forceFocus()
2156 }
2157
2158@@ -868,7 +985,9 @@
2159 if (!isReady) {
2160 messages.ready()
2161 }
2162- processPendingEvents()
2163+ markThreadAsRead()
2164+ if (!newMessage)
2165+ composeBar.forceFocus()
2166 }
2167
2168 // These fake items are used to track if there are instances loaded
2169@@ -912,6 +1031,10 @@
2170 property var participants: null
2171 property var account: null
2172 text: {
2173+ // FIXME: temporary workaround
2174+ if (account.protocolInfo.name == "irc") {
2175+ return i18n.tr("Join IRC Channel...")
2176+ }
2177 var protocolDisplayName = account.protocolInfo.serviceDisplayName;
2178 if (protocolDisplayName === "") {
2179 protocolDisplayName = account.protocolInfo.serviceName;
2180@@ -930,16 +1053,14 @@
2181 }
2182 actionList.actions = []
2183
2184- actionList.addAction(mmsGroupAction)
2185-
2186- for (var i in telepathyHelper.textAccounts.active) {
2187- var account = telepathyHelper.textAccounts.active[i]
2188- if (account.type == AccountEntry.PhoneAccount) {
2189- continue
2190- }
2191- var action = customGroupChatActionComponent.createObject(actionList, {"account": account, "participants": multiRecipient.participants})
2192- actionList.addAction(action)
2193- }
2194+ if (telepathyHelper.phoneAccounts.active.length > 0) {
2195+ actionList.addAction(mmsGroupAction)
2196+ }
2197+ if (!account || account.type == AccountEntry.PhoneAccount) {
2198+ return
2199+ }
2200+ var action = customGroupChatActionComponent.createObject(actionList, {"account": account, "participants": multiRecipient.participants})
2201+ actionList.addAction(action)
2202 }
2203 }
2204
2205@@ -970,7 +1091,7 @@
2206 }
2207
2208 onApplicationActiveChanged: {
2209- processPendingEvents()
2210+ markThreadAsRead()
2211 }
2212 }
2213
2214@@ -982,16 +1103,22 @@
2215 }
2216 }
2217
2218- ChatEntry {
2219- id: chatEntryObject
2220- chatType: messages.chatType
2221- participantIds: messages.participantIds
2222- chatId: messages.threadId
2223- accountId: messages.accountId
2224- autoRequest: !newMessage
2225-
2226+ Component {
2227+ id: chatEntryComponent
2228+
2229+ ChatEntry {
2230+ id: chatEntryObject
2231+ chatType: messages.chatType
2232+ participantIds: messages.participantIds
2233+ chatId: messages.threadId
2234+ accountId: messages.accountId
2235+ }
2236+ }
2237+
2238+ Connections {
2239+ target: messages.chatEntry
2240 onChatTypeChanged: {
2241- messages.chatType = chatEntryObject.chatType
2242+ messages.chatType = chatEntry.chatType
2243 }
2244
2245 onMessageSent: {
2246@@ -1008,9 +1135,15 @@
2247 }
2248 }
2249
2250+ Binding {
2251+ target: messages.chatEntry
2252+ property: "autoRequest"
2253+ value: !messages.newMessage && !messages.account.protocolInfo.enableRejoin
2254+ }
2255+
2256 Repeater {
2257- model: messages.chatEntry.chatStates
2258- Item {
2259+ model: account ? (account.protocolInfo.enableChatStates ? messages.chatEntry.chatStates : null) : null
2260+ delegate: Item {
2261 function processChatState() {
2262 if (modelData.state == ChatEntry.ChannelChatStateComposing) {
2263 messages.userTyping = true
2264@@ -1030,8 +1163,9 @@
2265
2266 ContactWatcher {
2267 id: typingContactWatcher
2268- identifier: messages.userTypingId
2269- addressableFields: messages.account ? messages.account.addressableVCardFields : ["tel"] // just to have a fallback there
2270+ identifier: messages.participantIdentifierByProtocol(messages.account, userTypingId)
2271+ addressableFields: messages.account ?
2272+ messages.contactMatchFieldFromProtocol(messages.account.protocolInfo.name, messages.account.addressableVCardFields) : []
2273 }
2274
2275 MessagesHeader {
2276@@ -1091,7 +1225,7 @@
2277 return account.accountId
2278 }
2279 // we just request presence on 1-1 chats
2280- identifier: participants.length == 1 ? participants[0].identifier : ""
2281+ identifier: participants && participants.length === 1 ? participants[0].identifier : ""
2282 }
2283
2284 ActivityIndicator {
2285@@ -1109,7 +1243,7 @@
2286
2287 property int resultCount: (status === Loader.Ready) ? item.count : 0
2288
2289- source: (multiRecipient.searchString !== "") && multiRecipient.focus ?
2290+ source: (multiRecipient.searchString !== "") ?
2291 Qt.resolvedUrl("ContactSearchList.qml") : ""
2292 clip: true
2293 visible: source != ""
2294@@ -1138,6 +1272,17 @@
2295 when: (searchListLoader.status === Loader.Ready)
2296 }
2297
2298+ Connections {
2299+ target: searchListLoader.item
2300+ onActiveFocusChanged: {
2301+ if (!searchListLoader.item.activeFocus && !multiRecipient.activeFocus)
2302+ multiRecipient.commit()
2303+ }
2304+ onFocusUp: {
2305+ multiRecipient.forceActiveFocus()
2306+ }
2307+ }
2308+
2309 Timer {
2310 id: checkHeight
2311
2312@@ -1168,12 +1313,13 @@
2313
2314 ContactWatcher {
2315 id: contactWatcherInternal
2316- identifier: firstParticipant ? firstParticipant.identifier : ""
2317- contactId: firstParticipant ? firstParticipant.contactId : ""
2318- alias: firstParticipant ? firstParticipant.alias : ""
2319- avatar: firstParticipant ? firstParticipant.avatar : ""
2320- detailProperties: firstParticipant ? firstParticipant.detailProperties : {}
2321- addressableFields: messages.account ? messages.account.addressableVCardFields : ["tel"] // just to have a fallback there
2322+ identifier: firstParticipant && firstParticipant.identifier ? messages.participantIdentifierByProtocol(messages.account, firstParticipant.identifier) : ""
2323+ contactId: firstParticipant && firstParticipant.contactId ? firstParticipant.contactId : ""
2324+ alias: firstParticipant && firstParticipant.alias ? firstParticipant.alias : ""
2325+ avatar: firstParticipant && firstParticipant.avatar ? firstParticipant.avatar : ""
2326+ detailProperties: firstParticipant && firstParticipant.detailProperties ? firstParticipant.detailProperties : {}
2327+ addressableFields: messages.account && messages.account.protocolInfo ?
2328+ messages.contactMatchFieldFromProtocol(messages.account.protocolInfo.name, messages.account.addressableVCardFields) : []
2329 }
2330
2331 HistoryUnionFilter {
2332@@ -1185,7 +1331,7 @@
2333 }
2334
2335 HistoryGroupedThreadsModel {
2336- id: threadsModel
2337+ id: messagesModel
2338 type: HistoryThreadModel.EventTypeText
2339 sort: HistorySort {}
2340 groupingProperty: "participants"
2341@@ -1200,7 +1346,7 @@
2342 property var localPendingParticipants: null
2343 property var remotePendingParticipants: null
2344 property var threads: null
2345- model: threadsModel
2346+ model: messagesModel
2347 visible: false
2348 delegate: Item {
2349 property var threads: model.threads
2350@@ -1218,12 +1364,13 @@
2351 id: eventModel
2352 type: HistoryThreadModel.EventTypeText
2353 filter: updateFilters(telepathyHelper.textAccounts.all, messages.chatType, messages.participantIds, messages.reloadFilters, messages.threads)
2354- matchContacts: true
2355+ matchContacts: messages.account ? messages.account.addressableVCardFields.length > 0 : false
2356 sort: HistorySort {
2357 sortField: "timestamp"
2358 sortOrder: HistorySort.DescendingOrder
2359 }
2360 onCountChanged: {
2361+ markThreadAsRead()
2362 if (isSearching) {
2363 // if we ask for more items manually listview will stop working,
2364 // so we only set again once the item was found
2365@@ -1278,12 +1425,21 @@
2366 objectName: "messageList"
2367 visible: !isSearching
2368 listModel: messages.newMessage ? null : eventModel
2369+ account: messages.account
2370+ activeFocusOnTab: false
2371+ focus: false
2372+ onActiveFocusChanged: {
2373+ if (activeFocus) {
2374+ composeBar.forceFocus()
2375+ }
2376+ }
2377
2378 Rectangle {
2379 color: Theme.palette.normal.background
2380 anchors.fill: parent
2381 Image {
2382 width: units.gu(20)
2383+ opacity: 0.1
2384 fillMode: Image.PreserveAspectFit
2385 anchors.centerIn: parent
2386 visible: source !== ""
2387@@ -1346,6 +1502,9 @@
2388 return false
2389 }
2390 if (threads.length > 0) {
2391+ if (!chatEntry.active && messages.account.protocolInfo.enableRejoin) {
2392+ return true
2393+ }
2394 return !threadInformation.chatRoomInfo.Joined
2395 }
2396 return false
2397@@ -1367,12 +1526,18 @@
2398 right: parent.right
2399 }
2400
2401+ participants: messages.participants
2402 isBroadcast: messages.isBroadcast
2403+ returnToSend: messages.account.protocolInfo.returnToSend
2404+ enableAttachments: messages.account.protocolInfo.enableAttachments
2405
2406 showContents: !selectionMode && !isSearching && !chatInactiveLabel.visible
2407 maxHeight: messages.height - keyboard.height - screenTop.y
2408 text: messages.text
2409 onTextChanged: {
2410+ if (!account.protocolInfo.enableChatStates) {
2411+ return
2412+ }
2413 if (text == "" && !composeBar.inputMethodComposing) {
2414 messages.chatEntry.setChatState(ChatEntry.ChannelChatStateActive)
2415 selfTypingTimer.stop()
2416@@ -1439,6 +1604,8 @@
2417 reloadFilters = !reloadFilters
2418 }
2419 }
2420+
2421+ KeyNavigation.up: messages.header.contents
2422 }
2423
2424 SendMessageValidator {
2425@@ -1474,4 +1641,17 @@
2426 flickableItem: messageList
2427 align: Qt.AlignTrailing
2428 }
2429+
2430+ Binding {
2431+ target: pageStack
2432+ property: "activePage"
2433+ value: messages
2434+ when: messages.active
2435+ }
2436+
2437+ onActiveFocusChanged: {
2438+ if (activeFocus && !newMessage) {
2439+ composeBar.textArea.forceActiveFocus()
2440+ }
2441+ }
2442 }
2443
2444=== modified file 'src/qml/MessagesListView.qml'
2445--- src/qml/MessagesListView.qml 2016-10-12 13:49:49 +0000
2446+++ src/qml/MessagesListView.qml 2017-03-27 19:24:23 +0000
2447@@ -30,6 +30,7 @@
2448
2449 property var _currentSwipedItem: null
2450 property string latestEventId: ""
2451+ property var account: null
2452
2453 function shareSelectedMessages()
2454 {
2455@@ -92,9 +93,17 @@
2456 var properties = {"messageData": model,
2457 "index": Qt.binding(function(){ return index }),
2458 "delegateItem": Qt.binding(function(){ return loader })}
2459- var sourceFile = textMessageType == HistoryThreadModel.MessageTypeInformation ? "AccountSectionDelegate.qml" : "RegularMessageDelegate.qml"
2460+ var sourceFile =textMessageType == HistoryThreadModel.MessageTypeInformation ? "AccountSectionDelegate.qml" : "RegularMessageDelegate.qml"
2461+ sourceFile = application.delegateFromProtocol(Qt.resolvedUrl(sourceFile), account ? account.protocolInfo.name : "")
2462 loader.setSource(sourceFile, properties)
2463 }
2464+
2465+ Binding {
2466+ target: loader.item
2467+ property: "account"
2468+ value: root.account
2469+ when: (textMessageType !== HistoryThreadModel.MessageTypeInformation && Loader.Ready)
2470+ }
2471 }
2472
2473 onSelectionDone: {
2474
2475=== modified file 'src/qml/MessagingContactEditorPage.qml'
2476--- src/qml/MessagingContactEditorPage.qml 2016-07-19 00:43:19 +0000
2477+++ src/qml/MessagingContactEditorPage.qml 2017-03-27 19:24:23 +0000
2478@@ -35,6 +35,7 @@
2479
2480 text: i18n.tr("Cancel")
2481 iconName: "back"
2482+ shortcut: "Esc"
2483 onTriggered: {
2484 root.cancel()
2485 root.active = false
2486@@ -47,18 +48,24 @@
2487
2488 text: i18n.tr("Save")
2489 iconName: "ok"
2490+ shortcut: "Ctrl+S"
2491 enabled: root.isContactValid
2492 onTriggered: root.save()
2493 }
2494 ]
2495
2496+ onActiveChanged: {
2497+ if (active)
2498+ forceActiveFocus()
2499+ }
2500+
2501 onContactSaved: {
2502 if (root.contactListPage) {
2503- if (root.contactListPage.phoneToAdd !== "") {
2504+ if (root.contactListPage.accountToAdd !== "") {
2505 mainStack.removePages(root.contactListPage)
2506 } else {
2507 root.contactListPage.moveListToContact(contact)
2508- root.contactListPage.phoneToAdd = ""
2509+ root.contactListPage.accountToAdd = null
2510 }
2511 }
2512 }
2513
2514=== modified file 'src/qml/MessagingContactViewPage.qml'
2515--- src/qml/MessagingContactViewPage.qml 2016-08-04 20:56:47 +0000
2516+++ src/qml/MessagingContactViewPage.qml 2017-03-27 19:24:23 +0000
2517@@ -1,4 +1,4 @@
2518-/*
2519+/*
2520 * Copyright 2015 Canonical Ltd.
2521 *
2522 * This file is part of messaging-app.
2523@@ -33,28 +33,64 @@
2524 objectName: "contactViewPage"
2525
2526 readonly property string contactEditorPageURL: Qt.resolvedUrl("MessagingContactEditorPage.qml")
2527- property string addPhoneToContact: ""
2528+ property var accountToAdd: null
2529 property var contactListPage: null
2530 model: null
2531
2532- function addPhoneToContactImpl(contact, phoneNumber)
2533+ function createContact(contact, detailName, newDetailSrc)
2534 {
2535- var detailSourceTemplate = "import QtContacts 5.0; PhoneNumber{ number: \"" + phoneNumber.trim() + "\" }"
2536- var newDetail = Qt.createQmlObject(detailSourceTemplate, contact)
2537+ var newDetail = Qt.createQmlObject(newDetailSrc, contact)
2538 if (newDetail) {
2539 contact.addDetail(newDetail)
2540 mainStack.addPageToCurrentColumn(root,
2541 root.contactEditorPageURL,
2542 { model: root.model,
2543 contact: contact,
2544- initialFocusSection: "phones",
2545+ initialFocusSection: detailName,
2546 newDetails: [newDetail],
2547 contactListPage: root.contactListPage })
2548- root.addPhoneToContact = ""
2549 } else {
2550- console.warn("Fail to create phone number detail")
2551- }
2552- }
2553+ console.warn("Fail to create contact with new detail")
2554+ }
2555+
2556+ }
2557+
2558+ function addPhoneToContactImpl(contact, phoneNumber)
2559+ {
2560+ var detailSourceTemplate = "import QtContacts 5.0; PhoneNumber{ number: \"" + phoneNumber.trim() + "\" }"
2561+ createContact(contact, "phones", detailSourceTemplate)
2562+ }
2563+
2564+ function addAccountToContactImpl(contact, account)
2565+ {
2566+ var detailSourceTemplate = "import QtContacts 5.0; OnlineAccount { protocol: " + account.protocol.trim() + "; accountUri: \"" + account.uri + "\" }"
2567+ createContact(contact, "ims", detailSourceTemplate)
2568+ }
2569+
2570+ function commit()
2571+ {
2572+ if (root.accountToAdd) {
2573+ if (root.accountToAdd.protocol === "OnlineAccount.Unknown") {
2574+ root.addPhoneToContactImpl(contact, root.accountToAdd.uri)
2575+ } else {
2576+ root.addAccountToContactImpl(contact, root.accountToAdd)
2577+ root.accountToAdd = null
2578+ }
2579+ }
2580+ }
2581+
2582+
2583+ leadingActions: [
2584+ Action {
2585+ objectName: "cancel"
2586+
2587+ text: i18n.tr("Cancel")
2588+ iconName: "back"
2589+ shortcut: "Esc"
2590+ onTriggered: pageStack.removePages(root)
2591+ }
2592+
2593+ ]
2594
2595 headerActions: [
2596 Action {
2597@@ -73,6 +109,7 @@
2598 text: i18n.tr("Edit")
2599 iconName: "edit"
2600 visible: root.editable
2601+ shortcut: "Ctrl+E"
2602 onTriggered: {
2603 pageStack.addPageToCurrentColumn(root, contactEditorPageURL,
2604 { model: root.model,
2605@@ -130,17 +167,13 @@
2606 onContactRemoved: pageStack.removePages(root)
2607 onContactFetched: {
2608 root.contact = contact
2609- if (root.active && root.addPhoneToContact != "") {
2610- root.addPhoneToContactImpl(contact, root.addPhoneToContact)
2611- root.addPhoneToContact = ""
2612- }
2613+ if (root.active)
2614+ root.commit()
2615 }
2616
2617 onActiveChanged: {
2618- if (active && root.contact && root.addPhoneToContact != "") {
2619- root.addPhoneToContactImpl(contact, root.addPhoneToContact)
2620- root.addPhoneToContact = ""
2621- }
2622+ if (active)
2623+ root.commit()
2624 }
2625
2626 Component.onCompleted: {
2627
2628=== modified file 'src/qml/MultiRecipientInput.qml'
2629--- src/qml/MultiRecipientInput.qml 2016-11-17 14:27:35 +0000
2630+++ src/qml/MultiRecipientInput.qml 2017-03-27 19:24:23 +0000
2631@@ -16,7 +16,7 @@
2632 * along with this program. If not, see <http://www.gnu.org/licenses/>.
2633 */
2634
2635-import QtQuick 2.2
2636+import QtQuick 2.4
2637 import Ubuntu.Components 1.3
2638 import Ubuntu.Contacts 0.1
2639 import Ubuntu.Telephony 0.1
2640@@ -30,13 +30,11 @@
2641 readonly property var participants: getParticipants()
2642 property string searchString: ""
2643 property var repeater: null
2644+ property string defaultHint: i18n.tr("To:")
2645+
2646 signal clearSearch()
2647- styleName: "TextFieldStyle"
2648- clip: true
2649- height: contactFlow.height
2650- focus: activeFocus
2651- property string defaultHint: i18n.tr("To:")
2652- onRecipientsChanged: getParticipants()
2653+ signal forceFocus()
2654+
2655 function getParticipants() {
2656 var participants = []
2657 var repeater = multiRecipientWidget.repeater
2658@@ -55,15 +53,6 @@
2659 return participants
2660 }
2661
2662- signal forceFocus()
2663-
2664- MouseArea {
2665- anchors.fill: scrollableArea
2666- enabled: parent.focus === false
2667- onClicked: forceFocus()
2668- z: 1
2669- }
2670-
2671 function addRecipient(identifier, contact) {
2672 for (var i = 0; i<recipientModel.count; i++) {
2673 // FIXME: replace by a phone number comparison method
2674@@ -77,6 +66,35 @@
2675 scrollableArea.contentX = contactFlow.width
2676 }
2677
2678+ function commit() {
2679+ for (var i=0; i < rpt.count; i++) {
2680+ var loader = rpt.itemAt(i)
2681+ if (loader.status !== Loader.Ready)
2682+ continue
2683+
2684+ var obj = loader.item
2685+ if (obj.objectName === "contactSearchInput") {
2686+ if (obj.text != "") {
2687+ addRecipient(obj.text)
2688+ obj.text = ""
2689+ }
2690+ }
2691+ }
2692+ }
2693+
2694+ onRecipientsChanged: getParticipants()
2695+ styleName: "TextFieldStyle"
2696+ clip: true
2697+ height: contactFlow.height
2698+ focus: activeFocus
2699+
2700+ MouseArea {
2701+ anchors.fill: scrollableArea
2702+ enabled: parent.focus === false
2703+ onClicked: forceFocus()
2704+ z: 1
2705+ }
2706+
2707 Behavior on height {
2708 UbuntuNumberAnimation {}
2709 }
2710@@ -193,12 +211,7 @@
2711 color: Theme.palette.normal.backgroundText
2712 font.pixelSize: FontUtils.sizeToPixels("medium")
2713 inputMethodHints: Qt.ImhNoPredictiveText
2714- onActiveFocusChanged: {
2715- if (!activeFocus && text !== "") {
2716- addRecipient(text)
2717- text = ""
2718- }
2719- }
2720+
2721 onTextChanged: {
2722 if (text.substring(text.length -1, text.length) == ",") {
2723 addRecipient(text.substring(0, text.length - 1))
2724@@ -207,6 +220,7 @@
2725 }
2726 searchString = text
2727 }
2728+
2729 Keys.onReturnPressed: {
2730 if (text == "")
2731 return
2732@@ -277,22 +291,19 @@
2733 }
2734 }
2735
2736- Icon {
2737+ TransparentButton {
2738 id: addIcon
2739- name: "add"
2740- height: units.gu(2)
2741+
2742+ iconName: "add"
2743+ height: units.gu(1.5)
2744 anchors {
2745 right: parent.right
2746 rightMargin: units.gu(2)
2747 verticalCenter: parent.verticalCenter
2748 }
2749- MouseArea {
2750- anchors.fill: parent
2751- anchors.margins: units.gu(-3)
2752- onClicked: {
2753+ onClicked: {
2754 Qt.inputMethod.hide()
2755 mainStack.addPageToCurrentColumn(messages, Qt.resolvedUrl("NewRecipientPage.qml"), {"itemCallback": multiRecipient})
2756- }
2757 }
2758 z: 2
2759 }
2760
2761=== modified file 'src/qml/NewGroupPage.qml'
2762--- src/qml/NewGroupPage.qml 2016-10-11 02:01:24 +0000
2763+++ src/qml/NewGroupPage.qml 2017-03-27 19:24:23 +0000
2764@@ -16,7 +16,7 @@
2765 * along with this program. If not, see <http://www.gnu.org/licenses/>.
2766 */
2767
2768-import QtQuick 2.0
2769+import QtQuick 2.4
2770 import Ubuntu.Components 1.3
2771 import Ubuntu.Components.ListItems 1.3 as ListItems
2772 import Ubuntu.History 0.1
2773@@ -29,6 +29,21 @@
2774 property bool creationInProgress: false
2775 property var participants: []
2776 property var account: null
2777+ readonly property bool allowCreateGroup: {
2778+ if (newGroupPage.creationInProgress) {
2779+ return false
2780+ }
2781+ if (account.protocolInfo.joinExistingChannels && groupTitleField.text != "") {
2782+ return true
2783+ }
2784+ if (participantsModel.count == 0) {
2785+ return false
2786+ }
2787+ if (!mmsGroup) {
2788+ return ((groupTitleField.text != "" || groupTitleField.inputMethodComposing) && participantsModel.count > 1)
2789+ }
2790+ return participantsModel.count > 1
2791+ }
2792
2793 function addRecipient(identifier, contact) {
2794 var alias = contact.displayLabel.label
2795@@ -49,6 +64,17 @@
2796 participantsModel.append({"identifier": identifier, "alias": alias, "avatar": avatar })
2797 }
2798
2799+ function commit() {
2800+ if (allowCreateGroup) {
2801+ Qt.inputMethod.commit()
2802+ newGroupPage.creationInProgress = true
2803+ if (account.protocolInfo.joinExistingChannels) {
2804+ chatEntry.chatId = groupTitleField.text
2805+ }
2806+ chatEntry.startChat()
2807+ }
2808+ }
2809+
2810 header: PageHeader {
2811 title: {
2812 if (creationInProgress) {
2813@@ -57,6 +83,10 @@
2814 if (mmsGroup) {
2815 return i18n.tr("New MMS Group")
2816 } else {
2817+ // FIXME: temporary workaround
2818+ if (account && account.protocolInfo.name == "irc") {
2819+ return i18n.tr("Join IRC channel:")
2820+ }
2821 var protocolDisplayName = account.protocolInfo.serviceDisplayName;
2822 if (protocolDisplayName === "") {
2823 protocolDisplayName = account.protocolInfo.serviceName;
2824@@ -69,6 +99,7 @@
2825 Action {
2826 objectName: "cancelAction"
2827 iconName: "close"
2828+ shortcut: "Esc"
2829 onTriggered: {
2830 Qt.inputMethod.commit()
2831 mainStack.removePages(newGroupPage)
2832@@ -79,25 +110,11 @@
2833 trailingActionBar {
2834 actions: [
2835 Action {
2836+ id: createAction
2837 objectName: "createAction"
2838- enabled: {
2839- if (newGroupPage.creationInProgress) {
2840- return false
2841- }
2842- if (participantsModel.count == 0) {
2843- return false
2844- }
2845- if (!mmsGroup) {
2846- return ((groupTitleField.text != "" || groupTitleField.inputMethodComposing) && participantsModel.count > 1)
2847- }
2848- return participantsModel.count > 1
2849- }
2850+ enabled: newGroupPage.allowCreateGroup
2851 iconName: "ok"
2852- onTriggered: {
2853- Qt.inputMethod.commit()
2854- newGroupPage.creationInProgress = true
2855- chatEntry.startChat()
2856- }
2857+ onTriggered: newGroupPage.commit()
2858 }
2859 ]
2860 }
2861@@ -209,7 +226,13 @@
2862 verticalAlignment: Text.AlignVCenter
2863 anchors.verticalCenter: groupTitleField.verticalCenter
2864 anchors.left: parent.left
2865- text: i18n.tr("Group name:")
2866+ text: {
2867+ // FIXME: temporary workaround
2868+ if (account && account.protocolInfo.name == "irc") {
2869+ return i18n.tr("Channel name:")
2870+ }
2871+ return i18n.tr("Group name:")
2872+ }
2873 }
2874 TextField {
2875 id: groupTitleField
2876@@ -221,8 +244,16 @@
2877 top: parent.top
2878 }
2879 height: units.gu(4)
2880- placeholderText: i18n.tr("Type a name...")
2881+ placeholderText: {
2882+ // FIXME: temporary workaround
2883+ if (account && account.protocolInfo.name == "irc") {
2884+ return i18n.tr("#channelName")
2885+ }
2886+ return i18n.tr("Type a name...")
2887+ }
2888 inputMethodHints: Qt.ImhNoPredictiveText
2889+ Keys.onReturnPressed: newGroupPage.commit()
2890+ Keys.onEnterPressed: newGroupPage.commit()
2891 Timer {
2892 interval: 1
2893 onTriggered: {
2894@@ -249,6 +280,7 @@
2895 ContactSearchWidget {
2896 id: searchItem
2897 parentPage: newGroupPage
2898+ visible: !account.protocolInfo.joinExistingChannels
2899 searchResultsHeight: flick.emptySpaceHeight
2900 onContactPicked: addRecipientFromSearch(identifier, alias, avatar)
2901 anchors {
2902@@ -259,6 +291,7 @@
2903 }
2904 Rectangle {
2905 id: separator2
2906+ visible: !account.protocolInfo.joinExistingChannels
2907 anchors {
2908 left: parent.left
2909 right: parent.right
2910@@ -285,6 +318,7 @@
2911 anchors.top: searchItem.bottom
2912 anchors.left: parent.left
2913 anchors.right: parent.right
2914+ visible: !account.protocolInfo.joinExistingChannels
2915 Repeater {
2916 id: participantsRepeater
2917 model: participantsModel
2918@@ -302,4 +336,11 @@
2919 KeyboardRectangle {
2920 id: keyboard
2921 }
2922+
2923+ onActiveChanged: {
2924+ if (active)
2925+ searchItem.forceActiveFocus()
2926+ }
2927+
2928+ Component.onCompleted: searchItem.forceActiveFocus()
2929 }
2930
2931=== modified file 'src/qml/NewRecipientPage.qml'
2932--- src/qml/NewRecipientPage.qml 2016-08-04 20:56:47 +0000
2933+++ src/qml/NewRecipientPage.qml 2017-03-27 19:24:23 +0000
2934@@ -26,7 +26,7 @@
2935 objectName: "newRecipientPage"
2936
2937 property var itemCallback: null
2938- property string phoneToAdd: ""
2939+ property var accountToAdd: null
2940 property QtObject contactIndex: null
2941
2942 function moveListToContact(contact)
2943@@ -50,6 +50,30 @@
2944 mainStack.removePages(newRecipientPage)
2945 }
2946
2947+ function createEmptyContactWithAccount(account, parent)
2948+ {
2949+ var details = [ {detail: "EmailAddress", field: "emailAddress", value: ""},
2950+ {detail: "Name", field: "firstName", value: ""}
2951+ ]
2952+
2953+ var newContact = Qt.createQmlObject("import QtContacts 5.0; Contact{ }", parent)
2954+ var detailSourceTemplate = "import QtContacts 5.0; %1{ %2: \"%3\" }"
2955+ for (var i=0; i < details.length; i++) {
2956+ var detailMetaData = details[i]
2957+ var newDetail = Qt.createQmlObject(detailSourceTemplate.arg(detailMetaData.detail)
2958+ .arg(detailMetaData.field)
2959+ .arg(detailMetaData.value), parent)
2960+ newContact.addDetail(newDetail)
2961+ }
2962+
2963+ var accountSourceTemplate = "import QtContacts 5.0; OnlineAccount{ accountUri: \"%1\"; protocol: %2 }"
2964+ var newDetail = Qt.createQmlObject(accountSourceTemplate
2965+ .arg(account.uri)
2966+ .arg(account.protocol), parent)
2967+ newContact.addDetail(newDetail)
2968+ return newContact
2969+ }
2970+
2971 header: PageHeader {
2972 id: pageHeader
2973
2974@@ -74,6 +98,8 @@
2975 }
2976 ]
2977 }
2978+
2979+
2980 }
2981
2982 Sections {
2983@@ -133,6 +159,8 @@
2984 Action {
2985 iconName: "back"
2986 text: i18n.tr("Cancel")
2987+ enabled: newRecipientPage.state == "searching"
2988+ shortcut: "Esc"
2989 onTriggered: {
2990 newRecipientPage.forceActiveFocus()
2991 newRecipientPage.state = "default"
2992@@ -171,6 +199,10 @@
2993 bottom: keyboard.top
2994 }
2995
2996+ focus: true
2997+ currentIndex: -1
2998+ highlightSelected: true
2999+ activeFocusOnTab: true
3000 showAddNewButton: true
3001 showImportOptions: (contactList.count === 0) && (filterTerm == "")
3002 // this will be used to callback the app, after create account
3003@@ -178,12 +210,13 @@
3004
3005 filterTerm: searchField.text
3006 onContactClicked: {
3007- if (newRecipientPage.phoneToAdd != "") {
3008- mainView.addPhoneToContact(newRecipientPage,
3009- contact,
3010- newRecipientPage.phoneToAdd,
3011- newRecipientPage,
3012- contactList.listModel)
3013+ if (newRecipientPage.accountToAdd) {
3014+ mainView.addAccountToContact(newRecipientPage,
3015+ contact,
3016+ accountToAdd.protocol,
3017+ accountToAdd.uri,
3018+ newRecipientPage,
3019+ contactList.listModel)
3020 } else {
3021 mainView.showContactDetails(newRecipientPage,
3022 contact,
3023@@ -193,12 +226,24 @@
3024 }
3025
3026 onAddNewContactClicked: {
3027- var newContact = ContactsJS.createEmptyContact(newRecipientPage.phoneToAdd, newRecipientPage)
3028+ var newContact = newRecipientPage.createEmptyContactWithAccount(newRecipientPage.accountToAdd, newRecipientPage)
3029+ var focusField = "name"
3030+ if (newRecipientPage.accountToAdd) {
3031+ switch (newRecipientPage.accountToAdd.protocol) {
3032+ case "ofono":
3033+ focusField = "phones"
3034+ break
3035+ default:
3036+ focusField = "ims"
3037+ break
3038+ }
3039+ }
3040+
3041 mainStack.addPageToCurrentColumn(newRecipientPage,
3042 Qt.resolvedUrl("MessagingContactEditorPage.qml"),
3043 { model: contactList.listModel,
3044 contact: newContact,
3045- initialFocusSection: (newRecipientPage.phoneToAdd != "" ? "phones" : "name"),
3046+ initialFocusSection: focusField,
3047 contactListPage: newRecipientPage })
3048 }
3049 }
3050@@ -211,6 +256,10 @@
3051 onActiveChanged: {
3052 if (active && (state === "searching")) {
3053 searchField.forceActiveFocus()
3054+ } else {
3055+ if (contactList.currentIndex === -1)
3056+ contactList.currentIndex = 0
3057+ contactList.forceActiveFocus()
3058 }
3059 }
3060
3061@@ -243,4 +292,19 @@
3062 }
3063 }
3064 }
3065+
3066+ // WORKAROUND: Wee need this button to register the "Esc" shortcut,
3067+ // adding it into the trailingActionBar cause the app to crash due a bug on SDK
3068+ Button {
3069+ visible: false
3070+ action: Action {
3071+ text: i18n.tr("Back")
3072+ enabled: newRecipientPage.active
3073+ shortcut: "Esc"
3074+ onTriggered: {
3075+ mainStack.removePages(newRecipientPage)
3076+ newRecipientPage.destroy()
3077+ }
3078+ }
3079+ }
3080 }
3081
3082=== added file 'src/qml/OnlineAccountsHelper.qml'
3083--- src/qml/OnlineAccountsHelper.qml 1970-01-01 00:00:00 +0000
3084+++ src/qml/OnlineAccountsHelper.qml 2017-03-27 19:24:23 +0000
3085@@ -0,0 +1,91 @@
3086+/*
3087+ * Copyright (C) 2014 Canonical, Ltd.
3088+ *
3089+ * This program is free software; you can redistribute it and/or modify
3090+ * it under the terms of the GNU General Public License as published by
3091+ * the Free Software Foundation; version 3.
3092+ *
3093+ * This program is distributed in the hope that it will be useful,
3094+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3095+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3096+ * GNU General Public License for more details.
3097+ *
3098+ * You should have received a copy of the GNU General Public License
3099+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
3100+ */
3101+
3102+import QtQuick 2.4
3103+import Ubuntu.Components 1.3
3104+import Ubuntu.OnlineAccounts 0.1
3105+import Ubuntu.OnlineAccounts.Client 0.1
3106+import Ubuntu.Components.Popups 1.3
3107+
3108+Item {
3109+ id: root
3110+
3111+ property var dialogInstance: null
3112+
3113+ function run(){
3114+ if (!root.dialogInstance) {
3115+ root.dialogInstance = PopupUtils.open(dialog)
3116+ }
3117+ }
3118+
3119+ Component {
3120+ id: dialog
3121+ Dialog {
3122+ id: dialogue
3123+ title: "Online Accounts"
3124+ text: i18n.tr("Pick an account to create.")
3125+
3126+ ScrollView {
3127+ width: dialog.width
3128+ height: Math.min(listView.count, 3) * units.gu(7)
3129+
3130+ ListView {
3131+ id: listView
3132+
3133+ anchors.fill: parent
3134+ clip: true
3135+ model: ProviderModel {
3136+ applicationId: "messaging-app"
3137+ }
3138+ delegate: ListItem {
3139+ ListItemLayout {
3140+ title.text: model.displayName
3141+
3142+ Image {
3143+ SlotsLayout.position: SlotsLayout.First
3144+ source: "image://theme/" + model.iconName
3145+ width: units.gu(5)
3146+ height: width
3147+ }
3148+ }
3149+ onClicked: {
3150+ listView.enabled = false
3151+ setup.providerId = model.providerId
3152+ setup.exec()
3153+ }
3154+ }
3155+ }
3156+ }
3157+ Button {
3158+ text: i18n.tr("Cancel")
3159+ onClicked: PopupUtils.close(dialogue)
3160+ }
3161+
3162+ Component.onDestruction: {
3163+ root.dialogInstance = null
3164+ }
3165+ }
3166+ }
3167+
3168+ Setup {
3169+ id: setup
3170+ applicationId: "messaging-app"
3171+ providerId: "irc"
3172+ onFinished: {
3173+ PopupUtils.close(root.dialogInstance)
3174+ }
3175+ }
3176+}
3177
3178=== modified file 'src/qml/ParticipantDelegate.qml'
3179--- src/qml/ParticipantDelegate.qml 2016-10-18 13:32:28 +0000
3180+++ src/qml/ParticipantDelegate.qml 2017-03-27 19:24:23 +0000
3181@@ -51,9 +51,10 @@
3182 id: avatar
3183 enabled: true
3184 fallbackAvatarUrl: {
3185- if (participant.avatar !== "") {
3186+ if (participant && participant.avatar && participant.avatar !== "") {
3187+ console.log(participant.avatar)
3188 return participant.avatar
3189- } else if (participant.alias === "") {
3190+ } else if (participant && participant.alias === "") {
3191 return "image://theme/contact"
3192 }
3193 return ""
3194
3195=== modified file 'src/qml/ParticipantInfoPage.qml'
3196--- src/qml/ParticipantInfoPage.qml 2016-10-07 13:28:15 +0000
3197+++ src/qml/ParticipantInfoPage.qml 2017-03-27 19:24:23 +0000
3198@@ -26,8 +26,9 @@
3199 property var delegate
3200 property var participant: delegate.participant
3201 property var chatEntry
3202+ property string protocolName: "ofono"
3203 property bool chatRoom: false
3204- property bool knownContact: participant.contactId !== ""
3205+ property bool knownContact: participant.contactId && (participant.contactId !== "")
3206
3207 header: PageHeader {
3208 id: pageHeader
3209@@ -37,7 +38,13 @@
3210
3211 Flickable {
3212 id: contentsFlickable
3213- anchors.fill: parent
3214+ anchors {
3215+ top: parent.top
3216+ bottom: buttons.top
3217+ left: parent.left
3218+ right: parent.right
3219+ }
3220+
3221 contentHeight: contentsColumn.height
3222 clip: true
3223
3224@@ -105,64 +112,77 @@
3225 }
3226 }
3227 }
3228-
3229- Item {
3230- id: padding
3231- height: units.gu(1)
3232- anchors.left: parent.left
3233- anchors.right: parent.right
3234- }
3235-
3236- ListItems.ThinDivider {
3237- anchors {
3238- left: parent.left
3239- right: parent.right
3240- }
3241- }
3242-
3243- Item {
3244- id: padding3
3245- height: units.gu(2)
3246- anchors.left: parent.left
3247- anchors.right: parent.right
3248- }
3249-
3250- Column {
3251- anchors {
3252- left: parent.left
3253- leftMargin: units.gu(2)
3254- }
3255- spacing: units.gu(2)
3256- Button {
3257- id: showInContactsButton
3258- text: knownContact ? i18n.tr("See in contacts") : i18n.tr("Add to contacts")
3259- onClicked: {
3260- if (knownContact) {
3261- mainView.showContactDetails(participantInfoPage, participant.contactId, null, null)
3262- } else {
3263- mainView.addPhoneToContact(participantInfoPage, "", participant.identifier, null, null)
3264- }
3265- }
3266- }
3267-
3268- Button {
3269- id: setAsAdminButton
3270- text: i18n.tr("Set as admin")
3271- visible: false
3272- // disabled until backends support this feature
3273- //visible: chatRoom && chatEntry.active && chatEntry.selfContactRoles == 3
3274- }
3275-
3276- Button {
3277- id: leaveButton
3278- visible: delegate.canRemove()
3279- text: i18n.tr("Remove from group")
3280- color: Theme.palette.normal.negative
3281- onClicked: {
3282- delegate.removeFromGroup()
3283- pageStack.removePages(participantInfoPage)
3284- }
3285- }
3286+ }
3287+ }
3288+
3289+ Column {
3290+ id: buttons
3291+ anchors {
3292+ left: parent.left
3293+ right: parent.right
3294+ bottom: parent.bottom
3295+ margins: units.gu(2)
3296+ }
3297+ spacing: units.gu(0.5)
3298+
3299+ Button {
3300+ id: showInContactsButton
3301+
3302+ anchors {
3303+ left: parent.left
3304+ right: parent.right
3305+ }
3306+ text: knownContact ? i18n.tr("See in contacts") : i18n.tr("Add to contacts")
3307+ onClicked: {
3308+ console.debug("Know contact:" + participant.contactId)
3309+ if (knownContact) {
3310+ mainView.showContactDetails(participantInfoPage, participant.contactId, null, null)
3311+ } else {
3312+ mainView.addAccountToContact(participantInfoPage, "", protocolName, participant.identifier, null, null)
3313+ }
3314+ }
3315+ }
3316+
3317+ Button {
3318+ id: setAsAdminButton
3319+
3320+ anchors {
3321+ left: parent.left
3322+ right: parent.right
3323+ }
3324+ text: i18n.tr("Set as admin")
3325+ visible: false
3326+ // disabled until backends support this feature
3327+ //visible: chatRoom && chatEntry.active && chatEntry.selfContactRoles == 3
3328+ }
3329+
3330+ Button {
3331+ id: sendMessageButton
3332+
3333+ anchors {
3334+ left: parent.left
3335+ right: parent.right
3336+ }
3337+ text: i18n.tr("Send private message")
3338+ onClicked: {
3339+ mainView.startChat({accountId: chatEntry.accountId, participantIds: [participant.identifier]})
3340+ pageStack.removePages(participantInfoPage)
3341+ }
3342+ }
3343+
3344+ Button {
3345+ id: leaveButton
3346+
3347+ anchors {
3348+ left: parent.left
3349+ right: parent.right
3350+ }
3351+ visible: delegate.canRemove()
3352+ text: i18n.tr("Remove from group")
3353+ color: Theme.palette.normal.negative
3354+ onClicked: {
3355+ delegate.removeFromGroup()
3356+ pageStack.removePages(participantInfoPage)
3357 }
3358 }
3359 }
3360
3361=== modified file 'src/qml/ParticipantsPopover.qml'
3362--- src/qml/ParticipantsPopover.qml 2016-10-06 07:41:14 +0000
3363+++ src/qml/ParticipantsPopover.qml 2017-03-27 19:24:23 +0000
3364@@ -16,53 +16,145 @@
3365 * along with this program. If not, see <http://www.gnu.org/licenses/>.
3366 */
3367
3368-import QtQuick 2.2
3369+import QtQuick 2.4
3370 import Ubuntu.Components 1.3
3371-import Ubuntu.Components.ListItems 1.3 as ListItem
3372 import Ubuntu.Components.Popups 1.3
3373 import Ubuntu.Contacts 0.1
3374 import Ubuntu.Telephony 0.1
3375
3376 import "dateUtils.js" as DateUtils
3377
3378-Popover {
3379- id: participantsPopover
3380+
3381+Item {
3382+ id: root
3383
3384 property variant participants: []
3385-
3386- anchorToKeyboard: false
3387- Column {
3388- id: containerLayout
3389- anchors {
3390- left: parent.left
3391- top: parent.top
3392- right: parent.right
3393- }
3394- Repeater {
3395- model: participants
3396- Item {
3397- height: childrenRect.height
3398- width: participantsPopover.width
3399- ListItem.Standard {
3400- id: participant
3401+ readonly property bool active: (_popover != null)
3402+ readonly property bool popupVisible: active && _popover.isPopup
3403+
3404+ property variant _popover: null
3405+ property var _sortedParticipants: []
3406+
3407+ function compareParticipants(p0, p1)
3408+ {
3409+ var i0 = String(p0.identifier).toLocaleLowerCase()
3410+ var i1 = String(p1.identifier).toLocaleLowerCase()
3411+
3412+ if (i0 < i1)
3413+ return -1
3414+ if (i0 > i1)
3415+ return 1
3416+ return 0
3417+ }
3418+
3419+ function close()
3420+ {
3421+ if (_popover) {
3422+ if (_popover.isPopup)
3423+ PopupUtils.close(_popover)
3424+ else
3425+ root._popover.destroy()
3426+ root._popover = null
3427+ }
3428+ }
3429+
3430+ function showParticpantsStartWith(parent, prefix, showPopup)
3431+ {
3432+ var filter = []
3433+ for(var i = 0; i < participants.length; i++) {
3434+ var valid = true
3435+ if (prefix.length !== 0) {
3436+ valid = String(participants[i].identifier).indexOf(prefix) === 0
3437+ }
3438+
3439+ if (valid) {
3440+ filter.push(participants[i])
3441+ }
3442+ }
3443+
3444+ root._sortedParticipants = filter
3445+ if (filter.length === 0 && popupVisible)
3446+ {
3447+ return ""
3448+ }
3449+
3450+ if ((filter.length === 1) && popupVisible)
3451+ {
3452+ return filter[0].identifier
3453+ }
3454+
3455+ if (_popover === null) {
3456+ if (showPopup)
3457+ _popover = PopupUtils.open(componentParticipantsPopover, parent)
3458+ else
3459+ _popover = nonVisualPopover.createObject(root, {"currentIndex": 0})
3460+
3461+ }
3462+
3463+ _popover.model = _sortedParticipants
3464+ return (filter.length > 0 ? filter[0].identifier : "")
3465+ }
3466+
3467+ function nextItem()
3468+ {
3469+ if (_popover === null)
3470+ return ""
3471+
3472+ var newIndex = -1
3473+ if (_popover.currentIndex < (_sortedParticipants.length - 1))
3474+ newIndex = _popover.currentIndex + 1
3475+ else
3476+ newIndex = 0
3477+
3478+ _popover.currentIndex = newIndex
3479+ return (_sortedParticipants[newIndex].identifier)
3480+ }
3481+
3482+ Component {
3483+ id: nonVisualPopover
3484+
3485+ QtObject {
3486+ property var model: view.model
3487+ property int currentIndex: -1
3488+ readonly property bool isPopup: false
3489+
3490+ Component.onDestruction: root._popover = null
3491+ }
3492+ }
3493+
3494+ Component {
3495+ id: componentParticipantsPopover
3496+
3497+ Popover {
3498+ id: participantsPopover
3499+
3500+ property alias model: view.model
3501+ property alias currentIndex: view.currentIndex
3502+ readonly property bool isPopup: true
3503+
3504+ UbuntuListView {
3505+ id: view
3506+
3507+ width: root.width
3508+ height: Math.min(contentHeight, root.height / 2)
3509+ model: []
3510+
3511+ delegate: ListItem {
3512 objectName: "participant%1".arg(index)
3513- text: contactWatcher.isUnknown ? contactWatcher.identifier : contactWatcher.alias
3514- onClicked: {
3515- PopupUtils.close(participantsPopover)
3516- mainView.startChat(contactWatcher.identifier)
3517+
3518+ width: view.width
3519+ height: layout.height
3520+ onClicked: root.selected(modelData)
3521+
3522+ ListItemLayout {
3523+ id: layout
3524+ title.text: modelData.identifier
3525 }
3526 }
3527- ContactWatcher {
3528- id: contactWatcher
3529- identifier: modelData.identifier
3530- contactId: modelData.contactId
3531- alias: modelData.alias
3532- avatar: modelData.avatar
3533- detailProperties: modelData.detailProperties
3534-
3535- addressableFields: messages.account.addressableVCardFields
3536- }
3537+ Keys.onEscapePressed: root.selected(null)
3538 }
3539+
3540+ Component.onDestruction: root._popover = null
3541 }
3542 }
3543 }
3544
3545=== modified file 'src/qml/RegularMessageDelegate.qml'
3546--- src/qml/RegularMessageDelegate.qml 2016-10-14 14:02:04 +0000
3547+++ src/qml/RegularMessageDelegate.qml 2017-03-27 19:24:23 +0000
3548@@ -36,6 +36,7 @@
3549 property var textMessage: messageData.textMessage
3550 property string accountId: messageData.accountId
3551 property int index: -1
3552+ property alias account: messageDelegate.account
3553
3554 // WORKAROUND: we can not use sections because the verticalLayoutDirection is ListView.BottomToTop the sections will appear
3555 // bellow the item
3556@@ -97,10 +98,5 @@
3557 root.startSelection()
3558 root.selectItem(delegateItem)
3559 }
3560- Component.onCompleted: {
3561- if (newEvent) {
3562- messages.markMessageAsRead(accountId, threadId, eventId, type);
3563- }
3564- }
3565 }
3566 }
3567
3568=== added file 'src/qml/RegularMessageDelegate_irc.qml'
3569--- src/qml/RegularMessageDelegate_irc.qml 1970-01-01 00:00:00 +0000
3570+++ src/qml/RegularMessageDelegate_irc.qml 2017-03-27 19:24:23 +0000
3571@@ -0,0 +1,200 @@
3572+/*
3573+ * Copyright 2012-2016 Canonical Ltd.
3574+ *
3575+ * This file is part of messaging-app.
3576+ *
3577+ * messaging-app is free software; you can redistribute it and/or modify
3578+ * it under the terms of the GNU General Public License as published by
3579+ * the Free Software Foundation; version 3.
3580+ *
3581+ * messaging-app is distributed in the hope that it will be useful,
3582+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
3583+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3584+ * GNU General Public License for more details.
3585+ *
3586+ * You should have received a copy of the GNU General Public License
3587+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
3588+ */
3589+
3590+import QtQuick 2.2
3591+import Ubuntu.Components 1.3
3592+import Ubuntu.Contacts 0.1
3593+import Ubuntu.History 0.1
3594+import Ubuntu.Telephony.PhoneNumber 0.1 as PhoneNumber
3595+
3596+import "3rd_party/ba-linkify.js" as BaLinkify
3597+
3598+ListItem {
3599+ id: messageDelegate
3600+ objectName: "messageDelegate"
3601+
3602+ // To be used by actions
3603+ property int _index: index
3604+
3605+ property var messageData: null
3606+ property string messageText: messageData ? messageData.textMessage : ""
3607+ property bool incoming: (messageData && messageData.senderId !== "self")
3608+ property string accountLabel: ""
3609+ property var account: null
3610+ property var _accountRegex: account && (account.selfContactId != "") ? new RegExp('\\b' + account.selfContactId + '\\b', 'g') : null
3611+
3612+ function getCountryCode() {
3613+ var localeName = Qt.locale().name
3614+ return localeName.substr(localeName.length - 2, 2)
3615+ }
3616+
3617+ function deleteMessage()
3618+ {
3619+ eventModel.removeEvents([messageData.properties]);
3620+ }
3621+
3622+ function forwardMessage()
3623+ {
3624+ var properties = {}
3625+ var items = [{"text": textMessage, "url":""}]
3626+ emptyStack()
3627+ var transfer = {}
3628+ transfer["items"] = items
3629+ properties["sharedAttachmentsTransfer"] = transfer
3630+
3631+ mainView.showMessagesView(properties)
3632+ }
3633+
3634+ function copyMessage()
3635+ {
3636+ Clipboard.push(messageText)
3637+ application.showNotificationMessage(i18n.tr("Text message copied to clipboard"), "edit-copy")
3638+ }
3639+
3640+ function resendMessage()
3641+ {
3642+ messages.validator.validateMessageAndSend(textMessage, messages.participantIds, [], {"x-canonical-tmp-files": true}, [messageDelegate.deleteMessage])
3643+ }
3644+
3645+ width: messageList.width
3646+ height: label.contentHeight
3647+ divider.visible: false
3648+ contentItem.clip: false
3649+
3650+ Label {
3651+ id: label
3652+
3653+ function parseText(text) {
3654+ if (!text) {
3655+ return text;
3656+ }
3657+
3658+ // remove html tags
3659+ text = text.replace(/</g,'&lt;').replace(/>/g,'<tt>&gt;</tt>');
3660+ // wrap text in a div to keep whitespaces and new lines from collapsing
3661+ text = '<div style="white-space: pre-wrap;">' + text + '</div>';
3662+ // check for links
3663+ var htmlText = BaLinkify.linkify(text);
3664+ if (htmlText !== text) {
3665+ return htmlText
3666+ }
3667+
3668+ // linkify phone numbers if no web links were found
3669+ var phoneNumbers = PhoneNumber.PhoneUtils.matchInText(text, getCountryCode())
3670+ for (var i = 0; i < phoneNumbers.length; ++i) {
3671+ var currentNumber = phoneNumbers[i]
3672+ text = text.replace(currentNumber, formatTelSchemeWith(currentNumber))
3673+ }
3674+
3675+ if ((messages.chatType !== HistoryThreadModel.ChatTypeRoom) ||
3676+ !messageDelegate.incoming ||
3677+ !_accountRegex) {
3678+ }
3679+
3680+ return text.replace(_accountRegex, "<b>" + account.selfContactId + "</b>")
3681+ }
3682+
3683+ property string sender: {
3684+ if (messageData.sender && incoming) {
3685+ if (messageData.sender.alias !== undefined && messageData.sender.alias !== "") {
3686+ return messageData.sender.alias
3687+ } else if (messageData.sender.identifier !== undefined && messageData.sender.identifier !== "") {
3688+ return messageData.sender.identifier
3689+ } else if (messageData.senderId !== "") {
3690+ return messageData.senderId
3691+ }
3692+ } else if (account.selfContactId == "") {
3693+ // Return first part of display name if account id is empty
3694+ var displayName = account.displayName.substring(0, account.displayName.indexOf('@'))
3695+ return displayName
3696+ } else {
3697+ return account.selfContactId
3698+ }
3699+ }
3700+
3701+
3702+ anchors {
3703+ left: parent.left
3704+ right: parent.right
3705+ margins: units.gu(1)
3706+ }
3707+ text: "%1 <font color=\"%2\">[%3]</font>\t%4"
3708+ .arg(Qt.formatTime(messageData.timestamp, Qt.DefaultLocaleShortDate))
3709+ .arg(incoming ? "green" : "blue")
3710+ .arg(sender)
3711+ .arg(parseText(messageDelegate.messageText))
3712+
3713+ wrapMode: Text.WordWrap
3714+
3715+ onLinkActivated: Qt.openUrlExternally(link)
3716+ }
3717+
3718+ leadingActions: ListItemActions {
3719+ actions: [
3720+ Action {
3721+ iconName: "delete"
3722+ text: i18n.tr("Delete")
3723+ onTriggered: deleteMessage()
3724+ }
3725+ ]
3726+ }
3727+
3728+ trailingActions: ListItemActions {
3729+ actions: [
3730+ Action {
3731+ id: retryAction
3732+
3733+ iconName: "reload"
3734+ text: i18n.tr("Retry")
3735+ visible: messageData.textMessageStatus === HistoryThreadModel.MessageStatusPermanentlyFailed
3736+ onTriggered: messageDelegate.resendMessage()
3737+ },
3738+ Action {
3739+ id: copyAction
3740+
3741+ iconName: "edit-copy"
3742+ text: i18n.tr("Copy")
3743+ visible: messageText !== ""
3744+ onTriggered: messageDelegate.copyMessage()
3745+ },
3746+ Action {
3747+ id: forwardAction
3748+
3749+ iconName: "mail-forward"
3750+ text: i18n.tr("Forward")
3751+ onTriggered: messageDelegate.forwardMessage()
3752+ },
3753+ Action {
3754+ id: infoAction
3755+
3756+ iconName: "info"
3757+ text: i18n.tr("Info")
3758+ onTriggered: {
3759+ var messageInfo = {"type": i18n.tr("IRC"),
3760+ "senderId": messageData.senderId,
3761+ "sender": messageData.sender,
3762+ "timestamp": messageData.timestamp,
3763+ "textReadTimestamp": messageData.textReadTimestamp,
3764+ "status": messageData.textMessageStatus,
3765+ "participants": messages.participants }
3766+ messageInfoDialog.showMessageInfo(messageInfo)
3767+ }
3768+ }
3769+ ]
3770+ }
3771+}
3772
3773=== modified file 'src/qml/SettingsPage.qml'
3774--- src/qml/SettingsPage.qml 2016-11-10 01:36:05 +0000
3775+++ src/qml/SettingsPage.qml 2017-03-27 19:24:23 +0000
3776@@ -18,21 +18,60 @@
3777
3778 import QtQuick 2.2
3779 import Ubuntu.Components 1.3
3780-import Ubuntu.Components.ListItems 1.3 as ListItem
3781+import Ubuntu.OnlineAccounts.Client 0.1
3782+import Qt.labs.settings 1.0
3783
3784 Page {
3785 id: settingsPage
3786 title: i18n.tr("Settings")
3787
3788- property var setMethods: {
3789- "mmsEnabled": function(value) { telepathyHelper.mmsEnabled = value }/*,
3790- "characterCountEnabled": function(value) { msgSettings.showCharacterCount = value }*/
3791- }
3792- property var settingsModel: [
3793- { "name": "mmsEnabled",
3794- "description": i18n.tr("Enable MMS messages"),
3795- "property": telepathyHelper.mmsEnabled
3796- }/*,
3797+ function createAccount()
3798+ {
3799+ if (onlineAccountHelper.item)
3800+ onlineAccountHelper.item.run()
3801+ }
3802+
3803+ readonly property var setMethods: {
3804+ "mmsEnabled": function(value) { telepathyHelper.mmsEnabled = value },
3805+ "threadSort": function(value) { mainView.sortThreadsBy = value },
3806+ "compactView": function(value) { mainView.compactView = value },
3807+ //"characterCountEnabled": function(value) { msgSettings.showCharacterCount = value }
3808+ }
3809+
3810+ property var sortByModel: {
3811+ "timestamp": i18n.tr("Sort by timestamp"),
3812+ "title": i18n.tr("Sort by title")
3813+ }
3814+
3815+ readonly property var settingsModel: [
3816+ { "type": "boolean",
3817+ "data": {"name": "mmsEnabled",
3818+ "description": i18n.tr("Enable MMS messages"),
3819+ "property": telepathyHelper.mmsEnabled,
3820+ "activatedFuncion": null,
3821+ "setMethod": "mmsEnabled"}
3822+ },
3823+ { "type": "boolean",
3824+ "data": {"name": "compactView",
3825+ "description": i18n.tr("Simplified conversation view"),
3826+ "property": mainView.compactView,
3827+ "activatedFuncion": null,
3828+ "setMethod": "compactView"}
3829+ },
3830+ { "type": "action",
3831+ "data": { "name": "addAccount",
3832+ "description": i18n.tr("Add an online account"),
3833+ "onActivated": "createAccount" }
3834+ },
3835+ { "type": "options",
3836+ "data": { "name": "threadSort",
3837+ "description": i18n.tr("Sort threads"),
3838+ "currentValue": mainView.sortThreadsBy,
3839+ "subtitle": settingsPage.sortByModel[mainView.sortThreadsBy],
3840+ "options": sortByModel,
3841+ "setMethod": "threadSort"}
3842+ }
3843+ /*,
3844 { "name": "characterCountEnabled",
3845 "description": i18n.tr("Show character count"),
3846 "property": msgSettings.showCharacterCount
3847@@ -53,47 +92,194 @@
3848 header: PageHeader {
3849 id: pageHeader
3850 title: settingsPage.title
3851- leadingActionBar {
3852- id: leadingBar
3853+ leadingActionBar.actions: [
3854+ Action {
3855+ iconName: "back"
3856+ text: i18n.tr("Back")
3857+ shortcut: "Esc"
3858+ onTriggered: mainView.emptyStack(true)
3859+ }
3860+ ]
3861+ flickable: settingsList
3862+ }
3863+
3864+ onActiveChanged: {
3865+ if (active) {
3866+ settingsList.forceActiveFocus()
3867 }
3868 }
3869
3870+
3871 Component {
3872 id: settingDelegate
3873- Item {
3874- anchors.left: parent.left
3875- anchors.right: parent.right
3876- height: units.gu(6)
3877- Label {
3878- id: descriptionLabel
3879- text: modelData.description
3880- anchors.left: parent.left
3881- anchors.right: checkbox.left
3882- anchors.verticalCenter: parent.verticalCenter
3883- anchors.leftMargin: units.gu(2)
3884- }
3885- Switch {
3886- id: checkbox
3887- objectName: modelData.name
3888- anchors.right: parent.right
3889- anchors.rightMargin: units.gu(2)
3890- anchors.verticalCenter: parent.verticalCenter
3891- checked: modelData.property
3892- onCheckedChanged: {
3893- if (checked != modelData.property) {
3894- settingsPage.setMethods[modelData.name](checked)
3895- }
3896- }
3897- }
3898- }
3899- }
3900-
3901- ListView {
3902+ ListItem {
3903+ onClicked: {
3904+ layoutDelegate.item.activate()
3905+ settingsList.currentIndex = index
3906+
3907+ }
3908+ ListItemLayout {
3909+ title.text: modelData.data.description
3910+ subtitle.text: modelData.data.subtitle ? modelData.data.subtitle : ""
3911+
3912+ Loader {
3913+ id: layoutDelegate
3914+
3915+ sourceComponent: {
3916+ switch(modelData.type) {
3917+ case "action":
3918+ return actionDelegate
3919+ case "boolean":
3920+ return booleanDelegate
3921+ case "options":
3922+ return optionsDelegate
3923+ }
3924+ }
3925+
3926+ Binding {
3927+ target: layoutDelegate.item
3928+ property: "modelData"
3929+ value: modelData.data
3930+ when: layoutDelegate.status === Loader.Ready
3931+ }
3932+ Binding {
3933+ target: layoutDelegate.item
3934+ property: "index"
3935+ value: index
3936+ when: layoutDelegate.status === Loader.Ready
3937+ }
3938+ }
3939+ }
3940+ }
3941+ }
3942+
3943+ Component {
3944+ id: booleanDelegate
3945+
3946+ CheckBox {
3947+ id: checkbox
3948+ objectName: modelData.name
3949+
3950+ property var modelData: null
3951+ property int index: -1
3952+
3953+ function activate()
3954+ {
3955+ checkbox.checked = !checkbox.checked
3956+ }
3957+
3958+ SlotsLayout.position: SlotsLayout.Trailing
3959+ checked: modelData.property
3960+ onCheckedChanged: {
3961+ if (checked != modelData.property) {
3962+ settingsPage.setMethods[modelData.setMethod](checked)
3963+ }
3964+ }
3965+ }
3966+ }
3967+
3968+ Component {
3969+ id: actionDelegate
3970+
3971+ ProgressionSlot {
3972+ id: progression
3973+ objectName: modelData.name
3974+
3975+ property var modelData: null
3976+ property int index: -1
3977+ function activate()
3978+ {
3979+ settingsPage[modelData.onActivated]()
3980+ }
3981+ }
3982+ }
3983+
3984+ Component {
3985+ id: optionsDelegate
3986+
3987+ ProgressionSlot {
3988+ id: progression
3989+ objectName: modelData.name
3990+
3991+ property var modelData: null
3992+ property int index: -1
3993+ function activate()
3994+ {
3995+ pageStack.addPageToNextColumn(settingsPage, optionsDelegatePage,
3996+ {"title": modelData.description,
3997+ "model": modelData.options,
3998+ "index": index,
3999+ "currentIndex": modelData.currentValue,
4000+ "setMethod": modelData.setMethod})
4001+ }
4002+ }
4003+ }
4004+
4005+ Component {
4006+ id: optionsDelegatePage
4007+
4008+ Page {
4009+ id: optionsPage
4010+
4011+ property alias title: pageHeader.title
4012+ property var model
4013+ property string currentIndex
4014+ property string setMethod
4015+ property int index: -1
4016+
4017+ signal selected(string key)
4018+
4019+ function indexOf(key) {
4020+ return Object.keys(optionsPage.model).indexOf(key)
4021+ }
4022+
4023+ onSelected: {
4024+ if (key !== "") {
4025+ settingsPage.setMethods[optionsPage.setMethod](key)
4026+ }
4027+ //WORKAROUND: re-set index of settings page because the list is
4028+ // rebuild after a value change and that cause the index to reset to 0
4029+ settingsList.currentIndex = index
4030+ pageStack.removePages(optionsPage)
4031+ }
4032+
4033+ header: PageHeader {
4034+ id: pageHeader
4035+
4036+ leadingActionBar.actions: [
4037+ Action {
4038+ iconName: "back"
4039+ text: i18n.tr("Back")
4040+ shortcut: "Esc"
4041+ onTriggered: optionsPage.selected("")
4042+ }
4043+ ]
4044+ flickable: pageView
4045+ }
4046+
4047+ UbuntuListView {
4048+ id: pageView
4049+
4050+ model: Object.keys(optionsPage.model)
4051+ anchors.fill: parent
4052+ currentIndex: optionsPage.indexOf(optionsPage.currentIndex)
4053+ delegate: ListItem {
4054+ ListItemLayout {
4055+ title.text: optionsPage.model[modelData]
4056+ }
4057+ onClicked: optionsPage.selected(modelData)
4058+ }
4059+ }
4060+
4061+ onActiveChanged: this.forceActiveFocus()
4062+ }
4063+ }
4064+
4065+ UbuntuListView {
4066+ id: settingsList
4067+
4068 anchors {
4069- top: pageHeader.bottom
4070- left: parent.left
4071- right: parent.right
4072- bottom: parent.bottom
4073+ fill: parent
4074 }
4075 model: settingsModel
4076 delegate: settingDelegate
4077@@ -102,7 +288,6 @@
4078 Loader {
4079 id: messagesBottomEdgeLoader
4080 active: mainView.dualPanel
4081- asynchronous: true
4082 /* FIXME: would be even more efficient to use setSource() to
4083 delay the compilation step but a bug in Qt prevents us.
4084 Ref.: https://bugreports.qt.io/browse/QTBUG-54657
4085@@ -114,4 +299,12 @@
4086 hint.height: 0
4087 }
4088 }
4089+
4090+ Loader {
4091+ id: onlineAccountHelper
4092+
4093+ anchors.fill: parent
4094+ asynchronous: true
4095+ source: Qt.resolvedUrl("OnlineAccountsHelper.qml")
4096+ }
4097 }
4098
4099=== modified file 'src/qml/ThreadDelegate.qml'
4100--- src/qml/ThreadDelegate.qml 2016-11-22 15:03:31 +0000
4101+++ src/qml/ThreadDelegate.qml 2017-03-27 19:24:23 +0000
4102@@ -28,6 +28,7 @@
4103 ListItem {
4104 id: delegate
4105
4106+ property bool compactView: false
4107 property var participant: participants ? participants[0] : {}
4108 property bool groupChat: chatType == HistoryThreadModel.ChatTypeRoom || participants.length > 1
4109 property string searchTerm
4110@@ -119,9 +120,46 @@
4111 }
4112 return formatDisplayedText(displayedEventTextMessage)
4113 }
4114+
4115+ state: compactView ? "compactView" : ""
4116+ states: [
4117+ State {
4118+ name: "compactView"
4119+ PropertyChanges {
4120+ target: avatar
4121+ visible: false
4122+ height: 0
4123+ width: 0
4124+ }
4125+ PropertyChanges {
4126+ target: delegate
4127+ height: units.gu(4)
4128+ }
4129+ PropertyChanges {
4130+ target: latestMessage
4131+ visible: false
4132+ }
4133+ PropertyChanges {
4134+ target: protocolIcon
4135+ visible: false
4136+ }
4137+ AnchorChanges {
4138+ target: unreadCountIndicator
4139+ anchors.left: contactName.left
4140+ anchors.verticalCenter: contactName.verticalCenter
4141+ anchors.right: undefined
4142+ anchors.top: undefined
4143+ anchors.bottom: undefined
4144+ }
4145+ AnchorChanges {
4146+ target: contactName
4147+ anchors.right: time.left
4148+ }
4149+ }
4150+ ]
4151 anchors.left: parent.left
4152 anchors.right: parent.right
4153- height: units.gu(10)
4154+ height: units.gu(8)
4155 divider.visible: false
4156 contentItem.anchors {
4157 leftMargin: units.gu(2)
4158@@ -194,11 +232,12 @@
4159 if (isBroadcast) {
4160 return Qt.resolvedUrl("assets/broadcast_icon.png")
4161 } else if (groupChat) {
4162- return Qt.resolvedUrl("assets/group_icon.png")
4163+ return "image://theme/contact-group"
4164 }
4165- return ""
4166+ return "image://theme/contact"
4167 }
4168 asynchronous: true
4169+ sourceSize.height: units.gu(2)
4170 }
4171
4172 Label {
4173@@ -207,12 +246,12 @@
4174 top: avatar.top
4175 topMargin: units.gu(0.5)
4176 left: chatTypeIcon.right
4177- leftMargin: chatTypeIcon.visible ? units.gu(0.5) : 0
4178 right: time.left
4179+ rightMargin: unreadCountIndicator.width
4180 }
4181 elide: Text.ElideRight
4182 color: Theme.palette.normal.backgroundText
4183- font.bold: unreadCountIndicator.visible
4184+ font.bold: unreadCount > 0
4185 text: {
4186 if (groupChat) {
4187 return groupChatLabel
4188@@ -285,7 +324,7 @@
4189 top: avatar.top
4190 topMargin: units.gu(-0.5)
4191 left: avatar.left
4192- leftMargin: units.gu(-0.5)
4193+ leftMargin: delegate.state == "compactView" ? contactName.paintedWidth + units.gu(0.5) : units.gu(-0.5)
4194 }
4195 z: 1
4196 visible: unreadCount > 0
4197@@ -334,10 +373,10 @@
4198
4199 Item {
4200 id: delegateHelper
4201- property string phoneNumber: participant.identifier
4202- property string alias: participant.alias ? participant.alias : ""
4203- property string avatar: participant.avatar ? participant.avatar : ""
4204- property string contactId: participant.contactId ? participant.contactId : ""
4205+ property string phoneNumber: participant ? participant.identifier : ""
4206+ property string alias: participant && participant.alias ? participant.alias : ""
4207+ property string avatar: participant && participant.avatar ? participant.avatar : ""
4208+ property string contactId: participant && participant.contactId ? participant.contactId : ""
4209 property alias subTypes: phoneDetail.subTypes
4210 property alias contexts: phoneDetail.contexts
4211 property bool isUnknown: contactId === ""
4212@@ -470,8 +509,8 @@
4213
4214 PhoneNumber {
4215 id: phoneDetail
4216- contexts: participant.phoneContexts ? participant.phoneContexts : []
4217- subTypes: participant.phoneSubTypes ? participant.phoneSubTypes : []
4218+ contexts: participant && participant.phoneContexts ? participant.phoneContexts : []
4219+ subTypes: participant && participant.phoneSubTypes ? participant.phoneSubTypes : []
4220 }
4221
4222 ContactDetailPhoneNumberTypeModel {
4223
4224=== modified file 'src/qml/ThreadsSectionDelegate.qml'
4225--- src/qml/ThreadsSectionDelegate.qml 2016-10-14 14:00:18 +0000
4226+++ src/qml/ThreadsSectionDelegate.qml 2017-03-27 19:24:23 +0000
4227@@ -23,6 +23,12 @@
4228
4229 Item {
4230 id: threadsSectionDelegate
4231+
4232+ function formatSectionTitle(title)
4233+ {
4234+ return title
4235+ }
4236+
4237 anchors {
4238 left: parent.left
4239 right: parent.right
4240@@ -32,7 +38,7 @@
4241 Label {
4242 anchors.fill: parent
4243 elide: Text.ElideRight
4244- text: DateUtils.friendlyDay(Qt.formatDate(section, "yyyy/MM/dd"), i18n);
4245+ text: formatSectionTitle(section)
4246 verticalAlignment: Text.AlignVCenter
4247 fontSize: "small"
4248 color: Theme.palette.normal.backgroundTertiaryText
4249
4250=== modified file 'src/qml/TransparentButton.qml'
4251--- src/qml/TransparentButton.qml 2016-06-27 11:59:26 +0000
4252+++ src/qml/TransparentButton.qml 2017-03-27 19:24:23 +0000
4253@@ -16,7 +16,7 @@
4254 * along with this program. If not, see <http://www.gnu.org/licenses/>.
4255 */
4256
4257-import QtQuick 2.0
4258+import QtQuick 2.4
4259 import Ubuntu.Components 1.3
4260
4261 Item {
4262@@ -45,6 +45,9 @@
4263 signal pressed()
4264 signal released()
4265
4266+ Keys.onEnterPressed: clicked()
4267+ Keys.onReturnPressed: clicked()
4268+
4269 Item {
4270 id: iconShape
4271 height: iconSize
4272@@ -103,4 +106,17 @@
4273 font.family: "Ubuntu"
4274 font.pixelSize: FontUtils.sizeToPixels("small")
4275 }
4276+
4277+ // draw focus border
4278+ activeFocusOnTab: true
4279+ Rectangle {
4280+ anchors {
4281+ fill: parent
4282+ margins: units.gu(-1)
4283+ }
4284+ border.color: Theme.palette.selected.focus
4285+ color: "transparent"
4286+ visible: parent.activeFocus
4287+ radius: 10
4288+ }
4289 }
4290
4291=== modified file 'src/qml/messaging-app.qml'
4292--- src/qml/messaging-app.qml 2016-10-21 12:22:44 +0000
4293+++ src/qml/messaging-app.qml 2017-03-27 19:24:23 +0000
4294@@ -19,6 +19,7 @@
4295 import QtQuick 2.2
4296 import QtQuick.Window 2.2
4297 import Qt.labs.settings 1.0
4298+import QtContacts 5.0
4299 import Ubuntu.Components 1.3
4300 import Ubuntu.Components.Popups 1.3
4301 import Ubuntu.Telephony 0.1
4302@@ -36,6 +37,14 @@
4303 property bool dualPanel: mainStack.columns > 1
4304 property bool composingNewMessage: activeMessagesView && activeMessagesView.newMessage
4305 property QtObject activeMessagesView: null
4306+ // settings
4307+ property alias sortThreadsBy: globalSettings.sortThreadsBy
4308+ property alias compactView: globalSettings.compactView
4309+ property alias favoriteChannels: favoriteChannelsItem
4310+
4311+ // private
4312+ property var _pendingProperties: null
4313+
4314
4315 function updateNewMessageStatus() {
4316 activeMessagesView = application.findMessagingChild("messagesPage", "active", true)
4317@@ -72,13 +81,32 @@
4318 initialProperties)
4319 }
4320
4321- function addPhoneToContact(currentPage, contact, phoneNumber, contactListPage, contactsModel) {
4322+ function protocolFromString(protocolName)
4323+ {
4324+ if (protocolName.indexOf("OnlineAccount.") === 0) {
4325+ // protocol already converted
4326+ return protocolName
4327+ }
4328+
4329+ switch(protocolName) {
4330+ case "irc":
4331+ return "OnlineAccount.Irc"
4332+ case "ofono":
4333+ default:
4334+ return "OnlineAccount.Unknown"
4335+ }
4336+ }
4337+
4338+ function addAccountToContact(currentPage, contact, accountProtocol, accountUri, contactListPage, contactsModel)
4339+ {
4340+ var accountDetails = {"protocol": protocolFromString(accountProtocol),
4341+ "uri": accountUri}
4342 if (contact === "") {
4343 mainStack.addPageToCurrentColumn(currentPage,
4344 Qt.resolvedUrl("NewRecipientPage.qml"),
4345- { "phoneToAdd": phoneNumber })
4346+ { "accountToAdd": accountDetails })
4347 } else {
4348- var initialProperties = { "addPhoneToContact": phoneNumber }
4349+ var initialProperties = { "accountToAdd": accountDetails }
4350 if (contactListPage) {
4351 initialProperties["contactListPage"] = contactListPage
4352 }
4353@@ -96,14 +124,6 @@
4354 }
4355 }
4356
4357- onApplicationActiveChanged: {
4358- if (applicationActive) {
4359- telepathyHelper.registerChannelObserver()
4360- } else {
4361- telepathyHelper.unregisterChannelObserver()
4362- }
4363- }
4364-
4365 function removeThreads(threads) {
4366 for (var i in threads) {
4367 var thread = threads[i];
4368@@ -114,6 +134,7 @@
4369 // and acknowledge all messages for the threads to be removed
4370 var properties = {'accountId': thread.accountId, 'threadId': thread.threadId,'participantIds': participants, 'chatType': thread.chatType}
4371 chatManager.acknowledgeAllMessages(properties)
4372+ chatManager.leaveRoom(properties, "")
4373 }
4374 // at last remove the threads
4375 threadModel.removeThreads(threads);
4376@@ -126,8 +147,36 @@
4377 mainView.showMessagesView(properties)
4378 }
4379
4380+ function connectToFavoriteChannels(account) {
4381+ var favs = favoriteChannels.getFavoriteChannels(account.accountId)
4382+ for (var c in favs) {
4383+ var favChannel = favs[c]
4384+ if (favChannel) {
4385+ console.debug("Start channel:" + account.accountId + "/" + favChannel)
4386+ var properties = {'chatType': HistoryThreadModel.ChatTypeRoom,
4387+ 'accountId': account.accountId,
4388+ 'threadId': favChannel}
4389+ chatManager.startChat(account.accountId, properties)
4390+ }
4391+ }
4392+ }
4393+
4394+ onApplicationActiveChanged: {
4395+ if (applicationActive) {
4396+ telepathyHelper.registerChannelObserver()
4397+ } else {
4398+ telepathyHelper.unregisterChannelObserver()
4399+ }
4400+ }
4401+
4402 Connections {
4403 target: telepathyHelper.textAccounts
4404+ onAccountChanged: {
4405+ if (active) {
4406+ connectToFavoriteChannels(entry)
4407+ }
4408+ }
4409+
4410 onActiveChanged: {
4411 for (var i in telepathyHelper.textAccounts.active) {
4412 if (telepathyHelper.textAccounts.active[i] == account) {
4413@@ -136,6 +185,7 @@
4414 }
4415 account = Qt.binding(defaultPhoneAccount)
4416 }
4417+
4418 }
4419
4420 Connections {
4421@@ -150,6 +200,9 @@
4422 !settings.mainViewIgnoreFirstTimeDialog && mainPage.displayedThreadIndex < 0) {
4423 PopupUtils.open(Qt.createComponent("Dialogs/NoDefaultSIMCardDialog.qml").createObject(mainView))
4424 }
4425+ for (var i in telepathyHelper.textAccounts.active) {
4426+ connectToFavoriteChannels(telepathyHelper.textAccounts.active[i])
4427+ }
4428 }
4429 }
4430
4431@@ -170,10 +223,44 @@
4432
4433 HistoryGroupedThreadsModel {
4434 id: threadModel
4435+
4436+ function indexOf(threadId, accountId) {
4437+ for (var i=0; i < count; i++) {
4438+ var threads = get(i)
4439+ for (var t=0; t < threads.length; t++) {
4440+ var thread = threads[t]
4441+ if (thread.threadId === threadId) {
4442+ if (accountId && (thread.accountId == accountId))
4443+ return i
4444+ else if (!accountId)
4445+ return i
4446+ }
4447+ }
4448+ }
4449+ return -1
4450+ }
4451+
4452 type: HistoryThreadModel.EventTypeText
4453 sort: HistorySort {
4454- sortField: "lastEventTimestamp"
4455- sortOrder: HistorySort.DescendingOrder
4456+ sortField: {
4457+ switch(mainView.sortThreadsBy) {
4458+ case "title":
4459+ //FIXME: ThreadId works for IRC, not sure if that will work for other protocols
4460+ return "accountId, threadId"
4461+ case "timestamp":
4462+ default:
4463+ return "lastEventTimestamp"
4464+ }
4465+ }
4466+ sortOrder: {
4467+ switch(mainView.sortThreadsBy) {
4468+ case "title":
4469+ return HistorySort.AscendingOrder
4470+ case "timestamp":
4471+ default:
4472+ return HistorySort.DescendingOrder
4473+ }
4474+ }
4475 }
4476 groupingProperty: "participants"
4477 filter: HistoryFilter {}
4478@@ -194,6 +281,12 @@
4479 property bool showCharacterCount: false
4480 }
4481
4482+ Settings {
4483+ id: globalSettings
4484+ property string sortThreadsBy: "timestamp"
4485+ property bool compactView: false
4486+ }
4487+
4488 StickerPacksModel {
4489 id: stickerPacksModel
4490 }
4491@@ -233,12 +326,13 @@
4492 if (showEmpty) {
4493 showEmptyState()
4494 }
4495- mainPage.displayedThreadIndex = -1
4496+ mainPage.forceActiveFocus()
4497 }
4498
4499 function showEmptyState() {
4500 if (mainStack.columns > 1 && !application.findMessagingChild("emptyStatePage")) {
4501 layout.addPageToNextColumn(mainPage, Qt.resolvedUrl("EmptyStatePage.qml"))
4502+ mainPage.displayedThreadIndex = -1
4503 }
4504 }
4505
4506@@ -317,7 +411,17 @@
4507 return threads
4508 }
4509
4510- function startChat(properties) {
4511+ function startChatLate(properties) {
4512+ if (!properties && !_pendingProperties)
4513+ return
4514+
4515+ if (!properties)
4516+ properties = _pendingProperties
4517+
4518+ // make sure that is called only once, disconnect
4519+ _pendingProperties = null
4520+ telepathyHelper.onSetupReady.disconnect(startChatLate)
4521+
4522 var participantIds = []
4523 var accountId = ""
4524 var match = HistoryThreadModel.MatchCaseSensitive
4525@@ -346,9 +450,39 @@
4526 }
4527 }
4528
4529+ // Try to select the corrent thread on thread list
4530+ accountId = properties.accountId
4531+ var threadId = properties.threadId
4532+ if (!threadId && (properties["threads"].length > 0)) {
4533+ threadId = properties["threads"][0].threadId
4534+ if (!accountId)
4535+ accountId = properties["threads"][0].accountId
4536+ }
4537+
4538+ if (threadId) {
4539+ var index = threadModel.indexOf(properties.threadId, accountId)
4540+ if (index !== -1) {
4541+ mainPage.selectMessage(index)
4542+ return
4543+ }
4544+ }
4545 showMessagesView(properties)
4546 }
4547
4548+ function startChat(properties) {
4549+ if (!telepathyHelper.ready) {
4550+ if (_pendingProperties) {
4551+ _pendingProperties = properties
4552+ } else {
4553+ _pendingProperties = properties
4554+ // wait for telepathy
4555+ telepathyHelper.onSetupReady.connect(startChatLate)
4556+ }
4557+ } else {
4558+ startChatLate(properties)
4559+ }
4560+ }
4561+
4562 Connections {
4563 target: UriHandler
4564 onOpened: {
4565@@ -360,6 +494,9 @@
4566
4567 AdaptivePageLayout {
4568 id: layout
4569+
4570+ property var activePage: null
4571+
4572 anchors.fill: parent
4573 layouts: PageColumnsLayout {
4574 when: mainStack.width >= units.gu(90)
4575@@ -398,4 +535,8 @@
4576 layout.completed = true;
4577 }
4578 }
4579+
4580+ FavoriteChannels {
4581+ id: favoriteChannelsItem
4582+ }
4583 }
4584
4585=== modified file 'tests/qml/tst_MessagesView.qml'
4586--- tests/qml/tst_MessagesView.qml 2016-11-04 18:24:21 +0000
4587+++ tests/qml/tst_MessagesView.qml 2017-03-27 19:24:23 +0000
4588@@ -75,6 +75,12 @@
4589
4590 Item {
4591 id: application
4592+
4593+ function delegateFromProtocol(delegate, protocol)
4594+ {
4595+ return delegate
4596+ }
4597+
4598 function findMessagingChild(name)
4599 {
4600 return null
4601@@ -132,15 +138,20 @@
4602 Item {
4603 id: chatManager
4604 signal messageAcknowledged
4605+ signal allMessagesAcknowledged(var properties)
4606 function acknowledgeMessage(recipients, messageId, accountId) {
4607 chatManager.messageAcknowledged(recipients, messageId, accountId)
4608 }
4609+
4610+ function acknowledgeAllMessages(properties) {
4611+ chatManager.allMessagesAcknowledged(properties)
4612+ }
4613 }
4614
4615 SignalSpy {
4616 id: messageAcknowledgeSpy
4617 target: chatManager
4618- signalName: "messageAcknowledged"
4619+ signalName: "allMessagesAcknowledged"
4620 }
4621
4622 Item {
4623@@ -150,6 +161,12 @@
4624 function updateNewMessageStatus() { }
4625 }
4626
4627+ Item {
4628+ id: threadModel
4629+ function markThreadsAsRead(threads) {
4630+ }
4631+ }
4632+
4633 Messages {
4634 id: messagesView
4635 active: true
4636@@ -192,6 +209,7 @@
4637 function test_messagesViewAcknowledgeMessage() {
4638 var senderId = "1234567"
4639 messagesView.participantIds = [senderId]
4640+ messagesView.threads = [ {threadId: "theThreadId"} ]
4641 var messageList
4642 while (true) {
4643 messageList = findChild(messagesView, "messageList")
4644@@ -205,7 +223,7 @@
4645 tryCompare(messageList, 'count', 2)
4646 compare(messageAcknowledgeSpy.count, 0)
4647 mainView.applicationActive = true
4648- tryCompare(messageAcknowledgeSpy, 'count', 2)
4649+ tryCompare(messageAcknowledgeSpy, 'count', 1)
4650 }
4651 }
4652 }

Subscribers

People subscribed via source and target branches

to all changes: