Merge lp:messaging-app/staging into lp:messaging-app
- staging
- Merge into trunk
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 |
Related bugs: |
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.
system-apps-ci-bot (system-apps-ci-bot) wrote : | # |
Gustavo Pichorim Boiko (boiko) wrote : | # |
The changes were reviewed individually and tested from this branch, so approving.
- 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
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,'<').replace(/>/g,'<tt>></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 | } |
PASSED: Continuous integration, rev:662 /jenkins. canonical. com/system- apps/job/ lp-messaging- app-ci/ 1/ /jenkins. canonical. com/system- apps/job/ build/2348 /jenkins. canonical. com/system- apps/job/ build-0- fetch/2348 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= xenial+ overlay/ 2166 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= xenial+ overlay/ 2166/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= zesty/2166 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=amd64, release= zesty/2166/ artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= xenial+ overlay/ 2166 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= xenial+ overlay/ 2166/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= zesty/2166 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=armhf, release= zesty/2166/ artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= xenial+ overlay/ 2166 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= xenial+ overlay/ 2166/artifact/ output/ *zip*/output. zip /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= zesty/2166 /jenkins. canonical. com/system- apps/job/ build-2- binpkg/ arch=i386, release= zesty/2166/ artifact/ output/ *zip*/output. zip
https:/
Executed test runs:
SUCCESS: https:/
SUCCESS: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
SUCCESS: https:/
deb: https:/
Click here to trigger a rebuild: /jenkins. canonical. com/system- apps/job/ lp-messaging- app-ci/ 1/rebuild
https:/