Merge lp:~phablet-team/messaging-app/side_panel into lp:messaging-app

Proposed by Gustavo Pichorim Boiko
Status: Superseded
Proposed branch: lp:~phablet-team/messaging-app/side_panel
Merge into: lp:messaging-app
Prerequisite: lp:~tiagosh/messaging-app/update-sim-dialogs
Diff against target: 8706 lines (+6166/-1165) (has conflicts)
78 files modified
.bzrignore (+37/-0)
debian/control (+5/-0)
debian/messaging-app.install (+1/-0)
src/CMakeLists.txt (+7/-1)
src/audiorecorder.cpp (+245/-0)
src/audiorecorder.h (+142/-0)
src/fileoperations.cpp (+58/-0)
src/fileoperations.h (+41/-0)
src/messagingapplication.cpp (+44/-1)
src/messagingapplication.h (+7/-1)
src/qml/AttachmentPanel.qml (+174/-0)
src/qml/AudioPlaybackBar.qml (+187/-0)
src/qml/AudioRecordingBar.qml (+137/-0)
src/qml/CMakeLists.txt (+1/-0)
src/qml/ComposeBar.qml (+534/-0)
src/qml/ContentImport.qml (+21/-4)
src/qml/DeliveryStatus.qml (+38/-0)
src/qml/Dialogs/FileSizeWarningDialog.qml (+70/-0)
src/qml/KeyboardRectangle.qml (+8/-0)
src/qml/LocalPageWithBottomEdge.qml (+3/-2)
src/qml/MMS/MMSAudio.qml (+200/-0)
src/qml/MMS/MMSBase.qml (+2/-0)
src/qml/MMS/MMSContact.qml (+15/-2)
src/qml/MMS/MMSImage.qml (+14/-0)
src/qml/MMS/MMSVideo.qml (+73/-43)
src/qml/MMS/Previewer.qml (+2/-2)
src/qml/MMS/PreviewerImage.qml (+183/-8)
src/qml/MMS/PreviewerVideo.qml (+124/-33)
src/qml/MMSDelegate.qml (+21/-18)
src/qml/MMSMessageBubble.qml (+1/-0)
src/qml/MainPage.qml (+77/-30)
src/qml/MessageBubble.qml (+63/-77)
src/qml/MessageDelegate.qml (+1/-0)
src/qml/MessageDelegateFactory.qml (+2/-0)
src/qml/Messages.qml (+538/-831)
src/qml/MessagesHeader.qml (+8/-3)
src/qml/MessagingContactEditorPage.qml (+41/-2)
src/qml/MessagingContactViewPage.qml (+42/-7)
src/qml/MultiRecipientInput.qml (+1/-1)
src/qml/NewRecipientPage.qml (+58/-35)
src/qml/SMSDelegate.qml (+1/-0)
src/qml/Stickers/CMakeLists.txt (+4/-0)
src/qml/Stickers/HistoryButton.qml (+35/-0)
src/qml/Stickers/StickerDelegate.qml (+32/-0)
src/qml/Stickers/StickerPackDelegate.qml (+53/-0)
src/qml/Stickers/StickerPacksModel.qml (+25/-0)
src/qml/Stickers/StickersModel.qml (+27/-0)
src/qml/Stickers/StickersPicker.qml (+138/-0)
src/qml/ThreadDelegate.qml (+64/-5)
src/qml/ThumbnailContact.qml (+106/-0)
src/qml/ThumbnailImage.qml (+49/-0)
src/qml/ThumbnailUnknown.qml (+45/-0)
src/qml/ThumbnailVideo.qml (+76/-0)
src/qml/TransparentButton.qml (+104/-0)
src/qml/assets/blue_bubble@27.sci (+5/-0)
src/qml/assets/burn-after-read.svg (+191/-0)
src/qml/assets/double_tick.svg (+21/-0)
src/qml/assets/face-smile-big-symbolic-2.svg (+182/-0)
src/qml/assets/green_bubble@27.sci (+5/-0)
src/qml/assets/grey_bubble@27.sci (+5/-0)
src/qml/assets/history.svg (+173/-0)
src/qml/assets/input-keyboard-symbolic.svg (+221/-0)
src/qml/assets/media_bubble@27.sci (+5/-0)
src/qml/assets/red_bubble@27.sci (+5/-0)
src/qml/assets/single_tick.svg (+20/-0)
src/qml/assets/stock_document.svg (+189/-0)
src/qml/assets/white_bubble@27.sci (+5/-0)
src/qml/dateUtils.js (+6/-1)
src/qml/messaging-app.qml (+50/-30)
src/stickers-history-model.cpp (+307/-0)
src/stickers-history-model.h (+91/-0)
tests/qml/CMakeLists.txt (+35/-25)
tests/qml/tst_MMSDelegate.qml (+196/-0)
tests/qml/tst_MessageBubble.qml (+3/-3)
tests/qml/tst_PreviewerImage.qml.disabled (+103/-0)
tests/qml/tst_PreviewerVideo.qml.disabled (+87/-0)
tests/qml/tst_QmlTests.cpp (+88/-0)
tests/qml/tst_StickersHistoryModel.qml (+188/-0)
Text conflict in src/qml/MessagingContactEditorPage.qml
Text conflict in src/qml/MessagingContactViewPage.qml
To merge this branch: bzr merge lp:~phablet-team/messaging-app/side_panel
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing
Ubuntu Phablet Team Pending
Review via email: mp+276528@code.launchpad.net

This proposal has been superseded by a proposal from 2016-01-14.

Commit message

Implement convergent layout for messaging-app.

Description of the change

Implement convergent layout for messaging-app.

To post a comment you must log in.
450. By Gustavo Pichorim Boiko

Re-add code removed by accident.

451. By Gustavo Pichorim Boiko

Remove code added by mistake when resolving conflicts.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
452. By Gustavo Pichorim Boiko

Merge latest changes from parent branch.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
453. By Gustavo Pichorim Boiko

Merge trunk.

454. By Gustavo Pichorim Boiko

Merge changes from IM branches.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
455. By Gustavo Pichorim Boiko

Merge latest IM changes.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
456. By Gustavo Pichorim Boiko

Update messaging-app to use the new SDK header in preparation to migrate to the
new bottom edge.

457. By Gustavo Pichorim Boiko

Make sure we always have a view on the right side of the screen so that we can
add a bottom edge gesture there.

458. By Gustavo Pichorim Boiko

Use the bottom edge from SDK.

459. By Gustavo Pichorim Boiko

Make it possible to create the bottom edge page using different properties.

460. By Gustavo Pichorim Boiko

Add a cancel action to the new message state to be used in the bottom edge pages.

461. By Gustavo Pichorim Boiko

Fix exposing the bottom edge page with arguments.

462. By Gustavo Pichorim Boiko

Merge trunk.

463. By Gustavo Pichorim Boiko

Fix qml tests.

464. By Gustavo Pichorim Boiko

Increase the waiting

465. By Gustavo Pichorim Boiko

Increase the waiting time even more.

466. By Gustavo Pichorim Boiko

Move the bottom edge action to the header on dual panel environments

467. By Gustavo Pichorim Boiko

Skip the header height from the contacts popup.

468. By Gustavo Pichorim Boiko

Remove the dependency on unity8-private

469. By Gustavo Pichorim Boiko

Remove debug leftover.

470. By Gustavo Pichorim Boiko

Show a new item on listview when the bottom edge page is visible.

471. By Gustavo Pichorim Boiko

Another attempt at scrolling the list to the new item.

472. By Gustavo Pichorim Boiko

Make sure we get the view on the test before proceeding.

473. By Gustavo Pichorim Boiko

Fix one more page header.

474. By Gustavo Pichorim Boiko

Fix one more pagestack interaction

475. By Gustavo Pichorim Boiko

Fix sending audio files.

476. By Gustavo Pichorim Boiko

Start fixing the interaction with header actions and the bottom edge in autopilot
tests.

477. By Gustavo Pichorim Boiko

Make pep8 happy.

478. By Gustavo Pichorim Boiko

Fix loading the contact viewing page.

479. By Gustavo Pichorim Boiko

Make sure the To: field gets focus when composing a new message.

480. By Gustavo Pichorim Boiko

Pages loaded in the bottom edge are not in the layout, so we need to use the
underneath page as the parent to push new pages to the stack.

481. By Gustavo Pichorim Boiko

Keep the bottom edge down arrow (back action) visible after sending the first
message.

482. By Tiago Salem Herrmann

merge apparmor branch

483. By Tiago Salem Herrmann

Fix ListView anchors to avoid hide the content under the header

484. By Gustavo Pichorim Boiko

Set the title correctly.

485. By Gustavo Pichorim Boiko

Make it possible to unselect all too in the main view.

486. By Gustavo Pichorim Boiko

Fix the header in the messages view.

487. By Gustavo Pichorim Boiko

Clear some warnings.

488. By Gustavo Pichorim Boiko

Make sure the page has a height when loading.

489. By Tiago Salem Herrmann

Fix ListView anchors also on NewRecipientPage

490. By Gustavo Pichorim Boiko

Create pages manually before pushing to the AdaptivePageLayout as it is very
slow to create the pages by itself.

491. By Gustavo Pichorim Boiko

Make sure pages get destroyed when hitting back

492. By Tiago Salem Herrmann

Implement back button to destroy dynamically created pages

493. By Tiago Salem Herrmann

Push all views synchronously

494. By Gustavo Pichorim Boiko

Make it possible to launch the new message view from settings.

495. By Tiago Salem Herrmann

fix anchors in the settings page

496. By Tiago Salem Herrmann

Empty stack before adding SettingsPage to the stack

497. By Tiago Salem Herrmann

Only set properties once during object creation
implement removePage() that destroy instances when needed

498. By Tiago Salem Herrmann

check before destroying instance

499. By Tiago Salem Herrmann

anchor previewer to header

500. By Gustavo Pichorim Boiko

Move the custom page layout functions to a separate QML file.

501. By Gustavo Pichorim Boiko

Merge swipe to cancel fix.

502. By Gustavo Pichorim Boiko

Remove duplicated code.

503. By Gustavo Pichorim Boiko

Fix loading pages by source

504. By Gustavo Pichorim Boiko

Hide the bottom edge bar in the empty state screen.

505. By Gustavo Pichorim Boiko

Messaging-app doesn't belong to side stage anymore

506. By Gustavo Pichorim Boiko

Set a minimum window size.

507. By Gustavo Pichorim Boiko

Make it landscape by default on windowed environments

508. By Gustavo Pichorim Boiko

Hide the new message list item once the message is sent.

509. By Gustavo Pichorim Boiko

Show the group chat participants popup anchored to the right

510. By Gustavo Pichorim Boiko

Make the header fixed in two column mode

511. By Gustavo Pichorim Boiko

Add a scrollbar to the Messages view.

512. By Gustavo Pichorim Boiko

Merge trunk.

513. By Gustavo Pichorim Boiko

Make the readme more clear

514. By Gustavo Pichorim Boiko

In dual panel mode, always hide the empty state label.

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file '.bzrignore'
2--- .bzrignore 1970-01-01 00:00:00 +0000
3+++ .bzrignore 2016-01-14 12:41:18 +0000
4@@ -0,0 +1,37 @@
5+*.qmlproject.user
6+CMakeCache.txt
7+CMakeFiles/
8+CMakeLists.txt.user
9+cmake_install.cmake
10+cmake_uninstall.cmake
11+Makefile
12+install_manifest.txt
13+CTestTestfile.cmake
14+
15+*.cbp
16+*.moc
17+moc_*.cpp
18+*_automoc.cpp
19+
20+Testing
21+RE:tests/qml/tst_\w+Tests$
22+po/*.gmo
23+po/src
24+
25+config.h
26+src/messaging-app
27+src/messaging-app.desktop*
28+
29+obj-*
30+debian/usr.bin.webbrowser-app
31+debian/files
32+debian/tmp/
33+debian/qtdeclarative5-ubuntu-ui-extras-browser-plugin/
34+debian/qtdeclarative5-ubuntu-web-plugin/
35+debian/qtdeclarative5-ubuntu-web-plugin-doc/
36+debian/messaging-app/
37+debian/messaging-app-autopilot/
38+debian/*.debhelper
39+debian/*.debhelper.log
40+debian/*.substvars
41+debian/stamp-*
42
43=== modified file 'debian/control'
44--- debian/control 2015-09-11 14:25:57 +0000
45+++ debian/control 2016-01-14 12:41:18 +0000
46@@ -21,8 +21,12 @@
47 qtdeclarative5-ubuntu-telephony0.1 | qtdeclarative5-ubuntu-telephony-plugin,
48 qtdeclarative5-ubuntu-content1,
49 qtdeclarative5-ubuntu-addressbook0.1,
50+ qtdeclarative5-ubuntu-thumbnailer0.1,
51 qtdeclarative5-qtcontacts-plugin,
52+ qtdeclarative5-folderlistmodel-plugin,
53+ qtmultimedia5-dev,
54 qml-module-qt-labs-settings,
55+ qml-module-qtmultimedia,
56 qtpim5-dev,
57 xvfb,
58 Standards-Version: 3.9.4
59@@ -37,6 +41,7 @@
60 Architecture: any
61 Depends: ${misc:Depends},
62 ${shlibs:Depends},
63+ libqt5multimedia5,
64 qtdeclarative5-ubuntu-addressbook0.1,
65 qtdeclarative5-ubuntu-ui-toolkit-plugin | qt-components-ubuntu,
66 qtdeclarative5-ubuntu-telephony-phonenumber0.1,
67
68=== modified file 'debian/messaging-app.install'
69--- debian/messaging-app.install 2014-08-11 22:22:59 +0000
70+++ debian/messaging-app.install 2016-01-14 12:41:18 +0000
71@@ -8,4 +8,5 @@
72 usr/share/messaging-app/3rd_party
73 usr/share/messaging-app/MMS/*.qml
74 usr/share/messaging-app/Dialogs/*.qml
75+usr/share/messaging-app/Stickers/*.qml
76 usr/bin/*messaging-app*
77
78=== modified file 'src/CMakeLists.txt'
79--- src/CMakeLists.txt 2015-02-24 13:33:31 +0000
80+++ src/CMakeLists.txt 2016-01-14 12:41:18 +0000
81@@ -1,18 +1,24 @@
82 set(MESSAGING_APP messaging-app)
83
84 set(messaging_app_HDRS
85+ audiorecorder.h
86+ fileoperations.h
87 messagingapplication.h
88+ stickers-history-model.h
89 )
90
91 set(messaging_app_SRCS
92+ audiorecorder.cpp
93+ fileoperations.cpp
94 messagingapplication.cpp
95 main.cpp
96+ stickers-history-model.cpp
97 )
98
99 add_executable(${MESSAGING_APP}
100 ${messaging_app_SRCS}
101 )
102-qt5_use_modules(${MESSAGING_APP} Core DBus Gui Qml Quick Versit)
103+qt5_use_modules(${MESSAGING_APP} Core DBus Gui Multimedia Qml Quick Sql Versit)
104
105 include_directories(
106 ${CMAKE_CURRENT_BINARY_DIR}
107
108=== added file 'src/audiorecorder.cpp'
109--- src/audiorecorder.cpp 1970-01-01 00:00:00 +0000
110+++ src/audiorecorder.cpp 2016-01-14 12:41:18 +0000
111@@ -0,0 +1,245 @@
112+/*
113+ * Copyright (C) 2015 Canonical, Ltd.
114+ *
115+ * Authors:
116+ * Arthur Renato Mello <arthur.mello@canonical.com>
117+ *
118+ * This file is part of messaging-app.
119+ *
120+ * messaging-app is free software; you can redistribute it and/or modify
121+ * it under the terms of the GNU General Public License as published by
122+ * the Free Software Foundation; version 3.
123+ *
124+ * messaging-app is distributed in the hope that it will be useful,
125+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
126+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
127+ * GNU General Public License for more details.
128+ *
129+ * You should have received a copy of the GNU General Public License
130+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
131+ */
132+
133+#include "audiorecorder.h"
134+
135+#include <QDebug>
136+#include <QDir>
137+#include <QUrl>
138+#include <QStandardPaths>
139+#include <QTemporaryFile>
140+
141+AudioRecorder::AudioRecorder(QObject *parent)
142+ : QObject(parent)
143+{
144+ m_audioRecorder = new QAudioRecorder();
145+ connect(m_audioRecorder, SIGNAL(stateChanged(QMediaRecorder::State)),
146+ SIGNAL(recorderStateChanged()));
147+ connect(m_audioRecorder, SIGNAL(statusChanged(QMediaRecorder::Status)),
148+ SIGNAL(recorderStatusChanged()));
149+ connect(m_audioRecorder, SIGNAL(error(QMediaRecorder::Error)),
150+ SLOT(updateRecorderError(QMediaRecorder::Error)));
151+ connect(m_audioRecorder, SIGNAL(actualLocationChanged(QUrl)),
152+ SLOT(updateActualLocation(QUrl)));
153+ connect(m_audioRecorder, SIGNAL(durationChanged(qint64)), SIGNAL(durationChanged(qint64)));
154+ connect(m_audioRecorder, SIGNAL(audioInputChanged(const QString&)),
155+ SIGNAL(audioInputChanged(const QString&)));
156+
157+ m_audioSettings = m_audioRecorder->audioSettings();
158+}
159+
160+AudioRecorder::~AudioRecorder()
161+{
162+ delete m_audioRecorder;
163+}
164+
165+AudioRecorder::RecorderState AudioRecorder::recorderState() const
166+{
167+ return RecorderState(m_audioRecorder->state());
168+}
169+
170+AudioRecorder::RecorderStatus AudioRecorder::recorderStatus() const
171+{
172+ return RecorderStatus(m_audioRecorder->status());
173+}
174+
175+AudioRecorder::Error AudioRecorder::errorCode() const
176+{
177+ return Error(m_audioRecorder->error());
178+}
179+
180+QString AudioRecorder::errorString() const
181+{
182+ return m_audioRecorder->errorString();
183+}
184+
185+QString AudioRecorder::outputLocation() const
186+{
187+ return m_audioRecorder->outputLocation().toString();
188+}
189+
190+QString AudioRecorder::actualLocation() const
191+{
192+ return m_audioRecorder->actualLocation().toString();
193+}
194+
195+qint64 AudioRecorder::duration() const
196+{
197+ return m_audioRecorder->duration();
198+}
199+
200+int AudioRecorder::bitRate() const
201+{
202+ return m_audioSettings.bitRate();
203+}
204+
205+int AudioRecorder::channelCount() const
206+{
207+ return m_audioSettings.channelCount();
208+}
209+
210+QString AudioRecorder::codec() const
211+{
212+ return m_audioSettings.codec();
213+}
214+
215+AudioRecorder::EncodingQuality AudioRecorder::quality() const
216+{
217+ return EncodingQuality(m_audioSettings.quality());
218+}
219+
220+int AudioRecorder::sampleRate() const
221+{
222+ return m_audioSettings.sampleRate();
223+}
224+
225+QString AudioRecorder::audioInput() const
226+{
227+ return m_audioRecorder->audioInput();
228+}
229+
230+void AudioRecorder::record()
231+{
232+ setRecorderState(RecordingState);
233+}
234+
235+void AudioRecorder::stop()
236+{
237+ setRecorderState(StoppedState);
238+}
239+
240+void AudioRecorder::pause()
241+{
242+ setRecorderState(PausedState);
243+}
244+
245+void AudioRecorder::setRecorderState(AudioRecorder::RecorderState state)
246+{
247+ if (!m_audioRecorder)
248+ return;
249+
250+ switch (state){
251+ case AudioRecorder::RecordingState: {
252+ // Create temporary file to store audio recorded
253+ QDir dataLocation(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
254+ QTemporaryFile outputFile(dataLocation.absoluteFilePath("audioXXXXXX%1").arg(m_fileExtension));
255+ outputFile.setAutoRemove(false);
256+ outputFile.open();
257+ outputFile.close();
258+ setOutputLocation(outputFile.fileName());
259+
260+ m_audioRecorder->record();
261+ break;
262+ }
263+ case AudioRecorder::StoppedState:
264+ m_audioRecorder->stop();
265+ break;
266+ case AudioRecorder::PausedState:
267+ m_audioRecorder->pause();
268+ break;
269+ }
270+}
271+
272+void AudioRecorder::setOutputLocation(const QString &location)
273+{
274+ if (outputLocation() != location) {
275+ // FIXME: implement auto-removal of previous recordings
276+ m_audioRecorder->setOutputLocation(location);
277+ Q_EMIT outputLocationChanged(outputLocation());
278+ }
279+}
280+
281+void AudioRecorder::setBitRate(int rate)
282+{
283+ if (bitRate() != rate) {
284+ m_audioSettings.setBitRate(rate);
285+ Q_EMIT bitRateChanged(rate);
286+ }
287+}
288+
289+void AudioRecorder::setChannelCount(int count)
290+{
291+ if (channelCount() != count) {
292+ m_audioSettings.setChannelCount(count);
293+ Q_EMIT channelCountChanged(count);
294+ }
295+}
296+
297+void AudioRecorder::setCodec(const QString &audioCodec)
298+{
299+ if (codec() != audioCodec) {
300+ if (!m_audioRecorder->supportedAudioCodecs().contains(audioCodec)) {
301+ qWarning() << "AudioRecorder error: Unsupported Audio Codec: " << audioCodec;
302+ return;
303+ }
304+
305+ if (audioCodec == "audio/vorbis" ||
306+ audioCodec == "audio/speex" ||
307+ audioCodec == "audio/FLAC") {
308+
309+ m_audioRecorder->setContainerFormat("ogg");
310+ m_fileExtension = ".ogg";
311+ } else if (audioCodec == "audio/PCM") {
312+ m_audioRecorder->setContainerFormat("wav");
313+ m_fileExtension = ".wav";
314+ } else {
315+ m_audioRecorder->setContainerFormat("raw");
316+ }
317+
318+ m_audioSettings.setCodec(audioCodec);
319+ Q_EMIT codecChanged(audioCodec);
320+ }
321+}
322+
323+void AudioRecorder::setQuality(AudioRecorder::EncodingQuality encodingQuality)
324+{
325+ if (quality() != encodingQuality) {
326+ m_audioSettings.setQuality(QMultimedia::EncodingQuality(encodingQuality));
327+ Q_EMIT qualityChanged(encodingQuality);
328+ }
329+}
330+
331+void AudioRecorder::setSampleRate(int rate)
332+{
333+ if (sampleRate() != rate) {
334+ m_audioSettings.setSampleRate(rate);
335+ Q_EMIT sampleRateChanged(rate);
336+ }
337+}
338+
339+void AudioRecorder::setAudioInput(const QString &input)
340+{
341+ if (audioInput() != input) {
342+ m_audioRecorder->setAudioInput(input);
343+ Q_EMIT audioInputChanged(input);
344+ }
345+}
346+
347+void AudioRecorder::updateRecorderError(QMediaRecorder::Error errorCode)
348+{
349+ qWarning() << "AudioRecorder error:" << errorString();
350+ Q_EMIT errorChanged(Error(errorCode), errorString());
351+}
352+
353+void AudioRecorder::updateActualLocation(const QUrl &url)
354+{
355+ Q_EMIT actualLocationChanged(url.toString());
356+}
357
358=== added file 'src/audiorecorder.h'
359--- src/audiorecorder.h 1970-01-01 00:00:00 +0000
360+++ src/audiorecorder.h 2016-01-14 12:41:18 +0000
361@@ -0,0 +1,142 @@
362+/*
363+ * Copyright (C) 2015 Canonical, Ltd.
364+ *
365+ * Authors:
366+ * Arthur Renato Mello <arthur.mello@canonical.com>
367+ *
368+ * This file is part of messaging-app.
369+ *
370+ * messaging-app is free software; you can redistribute it and/or modify
371+ * it under the terms of the GNU General Public License as published by
372+ * the Free Software Foundation; version 3.
373+ *
374+ * messaging-app is distributed in the hope that it will be useful,
375+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
376+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
377+ * GNU General Public License for more details.
378+ *
379+ * You should have received a copy of the GNU General Public License
380+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
381+ */
382+
383+#ifndef AUDIORECORDER_H
384+#define AUDIORECORDER_H
385+
386+#include <QObject>
387+#include <QAudioRecorder>
388+
389+class AudioRecorder : public QObject
390+{
391+ Q_OBJECT
392+
393+ Q_ENUMS(EncodingQuality)
394+ Q_ENUMS(Error)
395+ Q_ENUMS(RecorderState)
396+ Q_ENUMS(RecorderStatus)
397+
398+ Q_PROPERTY(RecorderState recorderState READ recorderState WRITE setRecorderState NOTIFY recorderStateChanged)
399+ Q_PROPERTY(RecorderStatus recorderStatus READ recorderStatus NOTIFY recorderStatusChanged)
400+ Q_PROPERTY(QString errorString READ errorString NOTIFY errorChanged)
401+ Q_PROPERTY(Error errorCode READ errorCode NOTIFY errorChanged)
402+ Q_PROPERTY(QString outputLocation READ outputLocation NOTIFY outputLocationChanged)
403+ Q_PROPERTY(QString actualLocation READ actualLocation NOTIFY actualLocationChanged)
404+ Q_PROPERTY(qint64 duration READ duration NOTIFY durationChanged)
405+ Q_PROPERTY(int bitRate READ bitRate WRITE setBitRate NOTIFY bitRateChanged);
406+ Q_PROPERTY(int channelCount READ channelCount WRITE setChannelCount NOTIFY channelCountChanged);
407+ Q_PROPERTY(QString codec READ codec WRITE setCodec NOTIFY codecChanged);
408+ Q_PROPERTY(EncodingQuality quality READ quality WRITE setQuality NOTIFY qualityChanged);
409+ Q_PROPERTY(int sampleRate READ sampleRate WRITE setSampleRate NOTIFY sampleRateChanged);
410+ Q_PROPERTY(QString audioInput READ audioInput WRITE setAudioInput NOTIFY audioInputChanged);
411+
412+public:
413+ enum EncodingQuality
414+ {
415+ VeryLowQuality = QMultimedia::VeryLowQuality,
416+ LowQuality = QMultimedia::LowQuality,
417+ NormalQuality = QMultimedia::NormalQuality,
418+ HighQuality = QMultimedia::HighQuality,
419+ VeryHighQuality = QMultimedia::VeryHighQuality
420+ };
421+
422+ enum Error
423+ {
424+ NoError = QMediaRecorder::NoError,
425+ ResourceError = QMediaRecorder::ResourceError,
426+ FormatError = QMediaRecorder::FormatError,
427+ OutOfSpaceError = QMediaRecorder::OutOfSpaceError
428+ };
429+
430+ enum RecorderState
431+ {
432+ StoppedState = QMediaRecorder::StoppedState,
433+ RecordingState = QMediaRecorder::RecordingState,
434+ PausedState = QMediaRecorder::PausedState
435+ };
436+
437+ enum RecorderStatus
438+ {
439+ UnavailableStatus = QMediaRecorder::UnavailableStatus,
440+ UnloadedStatus = QMediaRecorder::UnloadedStatus,
441+ LoadingStatus = QMediaRecorder::LoadingStatus,
442+ LoadedStatus = QMediaRecorder::LoadedStatus,
443+ StartingStatus = QMediaRecorder::StartingStatus,
444+ RecordingStatus = QMediaRecorder::RecordingStatus,
445+ PausedStatus = QMediaRecorder::PausedStatus,
446+ FinalizingStatus = QMediaRecorder::FinalizingStatus
447+ };
448+
449+ AudioRecorder(QObject *parent = 0);
450+ ~AudioRecorder();
451+
452+ RecorderState recorderState() const;
453+ RecorderStatus recorderStatus() const;
454+ Error errorCode() const;
455+ QString errorString() const;
456+ QString outputLocation() const;
457+ QString actualLocation() const;
458+ qint64 duration() const;
459+ int bitRate() const;
460+ int channelCount() const;
461+ QString codec() const;
462+ EncodingQuality quality() const;
463+ int sampleRate() const;
464+ QString audioInput() const;
465+
466+public Q_SLOTS:
467+ void record();
468+ void stop();
469+ void pause();
470+ void setRecorderState(AudioRecorder::RecorderState state);
471+ void setOutputLocation(const QString &location);
472+ void setBitRate(int rate);
473+ void setChannelCount(int count);
474+ void setCodec(const QString &audioCodec);
475+ void setQuality(AudioRecorder::EncodingQuality encodingQuality);
476+ void setSampleRate(int rate);
477+ void setAudioInput(const QString &input);
478+
479+Q_SIGNALS:
480+ void recorderStateChanged();
481+ void recorderStatusChanged();
482+ void errorChanged(AudioRecorder::Error errorCode, const QString &errorString);
483+ void outputLocationChanged(const QString &location);
484+ void actualLocationChanged(const QString &location);
485+ void durationChanged(qint64 duration);
486+ void bitRateChanged(int rate);
487+ void channelCountChanged(int count);
488+ void codecChanged(const QString &codec);
489+ void qualityChanged(AudioRecorder::EncodingQuality quality);
490+ void sampleRateChanged(int rate);
491+ void audioInputChanged(const QString &input);
492+
493+private Q_SLOTS:
494+ void updateRecorderError(QMediaRecorder::Error);
495+ void updateActualLocation(const QUrl&);
496+
497+private:
498+ QAudioRecorder *m_audioRecorder;
499+ QAudioEncoderSettings m_audioSettings;
500+ QString m_fileExtension;
501+};
502+
503+#endif // AUDIORECORDER_H
504
505=== added file 'src/fileoperations.cpp'
506--- src/fileoperations.cpp 1970-01-01 00:00:00 +0000
507+++ src/fileoperations.cpp 2016-01-14 12:41:18 +0000
508@@ -0,0 +1,58 @@
509+/*
510+ * Copyright (C) 2015 Canonical, Ltd.
511+ *
512+ * Authors:
513+ * Arthur Mello <arthur.mello@canonical.com>
514+ *
515+ * This file is part of messaging-app.
516+ *
517+ * messaging-app is free software; you can redistribute it and/or modify
518+ * it under the terms of the GNU General Public License as published by
519+ * the Free Software Foundation; version 3.
520+ *
521+ * messaging-app is distributed in the hope that it will be useful,
522+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
523+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
524+ * GNU General Public License for more details.
525+ *
526+ * You should have received a copy of the GNU General Public License
527+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
528+ */
529+
530+#include "fileoperations.h"
531+
532+#include <QDir>
533+#include <QFile>
534+#include <QTemporaryFile>
535+
536+FileOperations::FileOperations(QObject *parent)
537+ : QObject(parent)
538+{
539+}
540+
541+FileOperations::~FileOperations()
542+{
543+}
544+
545+QString FileOperations::getTemporaryFile(const QString &fileExtension) const
546+{
547+ //TODO remove once lp:1420728 is fixed
548+ QTemporaryFile tmp(QDir::tempPath() + "/tmpXXXXXX" + fileExtension);
549+ tmp.open();
550+ return tmp.fileName();
551+}
552+
553+bool FileOperations::link(const QString &from, const QString &to)
554+{
555+ return QFile::link(from, to);
556+}
557+
558+bool FileOperations::remove(const QString &fileName)
559+{
560+ return QFile::remove(fileName);
561+}
562+
563+qint64 FileOperations::size(const QString &filePath)
564+{
565+ return QFile(filePath).size();
566+}
567
568=== added file 'src/fileoperations.h'
569--- src/fileoperations.h 1970-01-01 00:00:00 +0000
570+++ src/fileoperations.h 2016-01-14 12:41:18 +0000
571@@ -0,0 +1,41 @@
572+/*
573+ * Copyright (C) 2015 Canonical, Ltd.
574+ *
575+ * Authors:
576+ * Arthur Mello <arthur.mello@canonical.com>
577+ *
578+ * This file is part of messaging-app.
579+ *
580+ * messaging-app is free software; you can redistribute it and/or modify
581+ * it under the terms of the GNU General Public License as published by
582+ * the Free Software Foundation; version 3.
583+ *
584+ * messaging-app is distributed in the hope that it will be useful,
585+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
586+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
587+ * GNU General Public License for more details.
588+ *
589+ * You should have received a copy of the GNU General Public License
590+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
591+ */
592+
593+#ifndef FILEOPERATIONS_H
594+#define FILEOPERATIONS_H
595+
596+#include <QObject>
597+
598+class FileOperations : public QObject
599+{
600+ Q_OBJECT
601+
602+public:
603+ FileOperations(QObject *parent = 0);
604+ ~FileOperations();
605+
606+ Q_INVOKABLE QString getTemporaryFile(const QString &fileExtension) const;
607+ Q_INVOKABLE bool link(const QString &from, const QString &to);
608+ Q_INVOKABLE bool remove(const QString &fileName);
609+ Q_INVOKABLE qint64 size(const QString &filePath);
610+};
611+
612+#endif // FILEOPERATIONS_H
613
614=== modified file 'src/messagingapplication.cpp'
615--- src/messagingapplication.cpp 2015-11-23 19:51:16 +0000
616+++ src/messagingapplication.cpp 2016-01-14 12:41:18 +0000
617@@ -1,5 +1,5 @@
618 /*
619- * Copyright (C) 2012 Canonical, Ltd.
620+ * Copyright (C) 2012-2015 Canonical, Ltd.
621 *
622 * This file is part of messaging-app.
623 *
624@@ -17,6 +17,9 @@
625 */
626
627 #include "messagingapplication.h"
628+#include "audiorecorder.h"
629+#include "fileoperations.h"
630+#include "stickers-history-model.h"
631
632 #include <libnotify/notify.h>
633
634@@ -24,6 +27,7 @@
635 #include <QUrl>
636 #include <QUrlQuery>
637 #include <QDebug>
638+#include <QDir>
639 #include <QStringList>
640 #include <QQuickItem>
641 #include <QQmlComponent>
642@@ -36,6 +40,7 @@
643 #include "config.h"
644 #include <QQmlEngine>
645 #include <QMimeDatabase>
646+#include <QStandardPaths>
647 #include <QVersitReader>
648
649 using namespace QtVersit;
650@@ -65,12 +70,31 @@
651 }
652 }
653
654+static QObject* FileOperations_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
655+{
656+ Q_UNUSED(engine);
657+ Q_UNUSED(scriptEngine);
658+ return new FileOperations();
659+}
660+
661+static QObject* StickersHistoryModel_singleton_factory(QQmlEngine* engine, QJSEngine* scriptEngine)
662+{
663+ Q_UNUSED(engine);
664+ Q_UNUSED(scriptEngine);
665+ return new StickersHistoryModel();
666+}
667+
668 MessagingApplication::MessagingApplication(int &argc, char **argv)
669 : QGuiApplication(argc, argv), m_view(0), m_applicationIsReady(false)
670 {
671 setApplicationName("MessagingApp");
672 }
673
674+bool MessagingApplication::fullscreen() const
675+{
676+ return m_view->windowState() == Qt::WindowFullScreen;
677+}
678+
679 bool MessagingApplication::setup()
680 {
681 installIconPath();
682@@ -149,6 +173,14 @@
683 m_view->rootContext()->setContextProperty("QTCONTACTS_MANAGER_OVERRIDE", contactsBackend);
684 }
685
686+ QDir dataLocation(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
687+ m_view->rootContext()->setContextProperty("dataLocation", dataLocation.absolutePath());
688+ dataLocation.mkpath("stickers");
689+ const char* uri = "messagingapp.private";
690+ qmlRegisterType<AudioRecorder>(uri, 0, 1, "AudioRecorder");
691+ qmlRegisterSingletonType<FileOperations>(uri, 0, 1, "FileOperations", FileOperations_singleton_factory);
692+ qmlRegisterSingletonType<StickersHistoryModel>(uri, 0, 1, "StickersHistoryModel", StickersHistoryModel_singleton_factory);
693+
694 // used by autopilot tests to load vcards during tests
695 QByteArray testData = qgetenv("QTCONTACTS_PRELOAD_VCARD");
696 m_view->rootContext()->setContextProperty("QTCONTACTS_PRELOAD_VCARD", testData);
697@@ -176,6 +208,17 @@
698 }
699 }
700
701+void MessagingApplication::setFullscreen(bool fullscreen)
702+{
703+ if (fullscreen) {
704+ m_view->setWindowState(Qt::WindowFullScreen);
705+ } else {
706+ m_view->setWindowState(Qt::WindowNoState);
707+ }
708+
709+ Q_EMIT fullscreenChanged();
710+}
711+
712 void MessagingApplication::onViewStatusChanged(QQuickView::Status status)
713 {
714 if (status != QQuickView::Ready) {
715
716=== modified file 'src/messagingapplication.h'
717--- src/messagingapplication.h 2015-11-18 16:36:51 +0000
718+++ src/messagingapplication.h 2016-01-14 12:41:18 +0000
719@@ -1,5 +1,5 @@
720 /*
721- * Copyright (C) 2012-2013 Canonical, Ltd.
722+ * Copyright (C) 2012-2015 Canonical, Ltd.
723 *
724 * This file is part of messaging-app.
725 *
726@@ -26,13 +26,18 @@
727 class MessagingApplication : public QGuiApplication
728 {
729 Q_OBJECT
730+ Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged)
731
732 public:
733 MessagingApplication(int &argc, char **argv);
734 virtual ~MessagingApplication();
735
736+ bool fullscreen() const;
737 bool setup();
738
739+Q_SIGNALS:
740+ void fullscreenChanged();
741+
742 public Q_SLOTS:
743 void activateWindow();
744 void parseArgument(const QString &arg);
745@@ -41,6 +46,7 @@
746 void showNotificationMessage(const QString &message, const QString &icon = QString());
747
748 private Q_SLOTS:
749+ void setFullscreen(bool fullscreen);
750 void onViewStatusChanged(QQuickView::Status status);
751 void onApplicationReady();
752
753
754=== added file 'src/qml/AttachmentPanel.qml'
755--- src/qml/AttachmentPanel.qml 1970-01-01 00:00:00 +0000
756+++ src/qml/AttachmentPanel.qml 2016-01-14 12:41:18 +0000
757@@ -0,0 +1,174 @@
758+/*
759+ * Copyright 2015 Canonical Ltd.
760+ *
761+ * This file is part of messaging-app.
762+ *
763+ * messaging-app is free software; you can redistribute it and/or modify
764+ * it under the terms of the GNU General Public License as published by
765+ * the Free Software Foundation; version 3.
766+ *
767+ * messaging-app is distributed in the hope that it will be useful,
768+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
769+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
770+ * GNU General Public License for more details.
771+ *
772+ * You should have received a copy of the GNU General Public License
773+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
774+ */
775+
776+import QtQuick 2.0
777+import Ubuntu.Components 1.3
778+import Ubuntu.Components.ListItems 1.3 as ListItem
779+import QtQuick.Layouts 1.0
780+
781+Item {
782+ id: panel
783+ signal attachmentAvailable(var attachment)
784+
785+ property bool expanded: false
786+
787+ function show() {
788+ expanded = true
789+ }
790+
791+ function hide() {
792+ expanded = false
793+ }
794+
795+ height: expanded ? childrenRect.height + units.gu(3): 0
796+ opacity: expanded ? 1 : 0
797+ visible: opacity > 0
798+ Behavior on height {
799+ UbuntuNumberAnimation {}
800+ }
801+ Behavior on opacity {
802+ UbuntuNumberAnimation { }
803+ }
804+
805+ enabled: expanded
806+
807+ Connections {
808+ target: Qt.inputMethod
809+ onVisibleChanged: {
810+ if (Qt.inputMethod.visible) {
811+ panel.expanded = false
812+ }
813+ }
814+ }
815+
816+ ContentImport {
817+ id: contentImporter
818+
819+ onContentReceived: {
820+ var attachment = {}
821+ var filePath = String(contentUrl).replace('file://', '')
822+ attachment["contentType"] = application.fileMimeType(filePath)
823+ attachment["name"] = filePath.split('/').reverse()[0]
824+ attachment["filePath"] = filePath
825+ panel.attachmentAvailable(attachment)
826+ hide()
827+ }
828+ }
829+
830+ ListItem.ThinDivider {
831+ id: divider
832+ anchors {
833+ top: parent.top
834+ left: parent.left
835+ right: parent.right
836+ }
837+ }
838+
839+ GridLayout {
840+ id: grid
841+
842+ property int iconSize: units.gu(3)
843+ property int buttonSpacing: units.gu(2)
844+ anchors {
845+ top: parent.top
846+ topMargin: units.gu(3)
847+ left: parent.left
848+ right: parent.right
849+ }
850+
851+ height: childrenRect.height
852+ columns: 4
853+ rowSpacing: units.gu(3)
854+
855+ TransparentButton {
856+ id: pictureButton
857+ objectName: "pictureButton"
858+ iconName: "stock_image"
859+ iconSize: grid.iconSize
860+ spacing: grid.buttonSpacing
861+ text: i18n.tr("Image")
862+ Layout.alignment: Qt.AlignHCenter
863+ onClicked: {
864+ contentImporter.requestPicture()
865+ }
866+ }
867+
868+ TransparentButton {
869+ id: videoButton
870+ objectName: "videoButton"
871+ iconName: "stock_video"
872+ iconSize: grid.iconSize
873+ spacing: grid.buttonSpacing
874+ text: i18n.tr("Video")
875+ Layout.alignment: Qt.AlignHCenter
876+ onClicked: {
877+ contentImporter.requestVideo()
878+ }
879+ }
880+
881+ // FIXME: enable generic file sharing if we ever support it
882+ /*TransparentButton {
883+ id: fileButton
884+ objectName: "fileButton"
885+ iconSource: Qt.resolvedUrl("assets/stock_document.svg")
886+ iconSize: grid.iconSize
887+ spacing: grid.buttonSpacing
888+ text: i18n.tr("File")
889+ Layout.alignment: Qt.AlignHCenter
890+ onClicked: {
891+ contentImporter.requestDocument()
892+ }
893+ }*/
894+
895+ // FIXME: enable location sharing if we ever support it
896+ /*TransparentButton {
897+ id: locationButton
898+ objectName: "locationButton"
899+ iconName: "location"
900+ iconSize: grid.iconSize
901+ spacing: grid.buttonSpacing
902+ text: i18n.tr("Location")
903+ Layout.alignment: Qt.AlignHCenter
904+ }*/
905+
906+ TransparentButton {
907+ id: contactButton
908+ objectName: "contactButton"
909+ iconName: "stock_contact"
910+ iconSize: grid.iconSize
911+ spacing: grid.buttonSpacing
912+ text: i18n.tr("Contact")
913+ Layout.alignment: Qt.AlignHCenter
914+ onClicked: {
915+ contentImporter.requestContact()
916+ }
917+ }
918+
919+ // FIXME: enable that once we add support for burn-after-read
920+ /*TransparentButton {
921+ id: burnAfterReadButton
922+ objectName: "burnAfterReadButton"
923+ iconSource: Qt.resolvedUrl("assets/burn-after-read.svg")
924+ iconSize: grid.iconSize
925+ spacing: grid.buttonSpacing
926+ text: i18n.tr("Burn after read")
927+ Layout.alignment: Qt.AlignHCenter
928+ }*/
929+ }
930+}
931+
932
933=== added file 'src/qml/AudioPlaybackBar.qml'
934--- src/qml/AudioPlaybackBar.qml 1970-01-01 00:00:00 +0000
935+++ src/qml/AudioPlaybackBar.qml 2016-01-14 12:41:18 +0000
936@@ -0,0 +1,187 @@
937+/*
938+ * Copyright 2015 Canonical Ltd.
939+ *
940+ * This file is part of messaging-app.
941+ *
942+ * messaging-app is free software; you can redistribute it and/or modify
943+ * it under the terms of the GNU General Public License as published by
944+ * the Free Software Foundation; version 3.
945+ *
946+ * messaging-app is distributed in the hope that it will be useful,
947+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
948+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
949+ * GNU General Public License for more details.
950+ *
951+ * You should have received a copy of the GNU General Public License
952+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
953+ */
954+
955+import QtQuick 2.0
956+import QtMultimedia 5.0
957+import Ubuntu.Components 1.3
958+import Ubuntu.Components.Themes.Ambiance 1.3
959+import "dateUtils.js" as DateUtils
960+
961+Item {
962+ id: playbackBar
963+
964+ signal resetRequested()
965+ property string source: ""
966+ property int duration: audioPlayer.duration
967+ readonly property bool playing: audioPlayer.playing
968+
969+ Loader {
970+ id: audioPlayer
971+ readonly property bool playing: ready ? item.playing : false
972+ readonly property bool paused: ready ? item.paused : false
973+ readonly property bool stopped: ready ? item.stopped : false
974+ readonly property int position: ready ? item.position : 0
975+ readonly property int duration: ready ? item.duration : 0
976+ readonly property bool ready: status == Loader.Ready
977+ readonly property int playbackState: ready ? item.playbackState : Audio.StoppedState
978+ function play() {
979+ audioPlayer.active = true
980+ item.play()
981+ }
982+ function stop() {
983+ item.stop()
984+ audioPlayer.active = false
985+ }
986+ function pause() { item.pause() }
987+ function seek(pos) { item.seek(pos) }
988+ active: false
989+ sourceComponent: audioPlayerComponent
990+ }
991+
992+ Component {
993+ id: audioPlayerComponent
994+ Audio {
995+ id: audioPlayer1
996+ readonly property bool playing: audioPlayer1.playbackState == Audio.PlayingState
997+ readonly property bool paused: audioPlayer1.playbackState == Audio.PausedState
998+ readonly property bool stopped: audioPlayer1.playbackState == Audio.StoppedState
999+ source: playbackBar.source
1000+ }
1001+ }
1002+
1003+ TransparentButton {
1004+ id: closeButton
1005+ objectName: "closeButton"
1006+
1007+ anchors {
1008+ left: parent.left
1009+ leftMargin: units.gu(2)
1010+ verticalCenter: parent.verticalCenter
1011+ }
1012+
1013+ iconName: "close"
1014+
1015+ onClicked: {
1016+ playbackBar.resetRequested()
1017+ }
1018+ }
1019+
1020+ Item {
1021+ id: audioPreview
1022+ anchors {
1023+ top: parent.top
1024+ bottom: parent.bottom
1025+ left: closeButton.right
1026+ right: parent.right
1027+ topMargin: units.gu(1)
1028+ bottomMargin: units.gu(1)
1029+ leftMargin: units.gu(3)
1030+ rightMargin: units.gu(1)
1031+ }
1032+
1033+ TransparentButton {
1034+ id: playButton
1035+
1036+ anchors {
1037+ top: parent.top
1038+ left: parent.left
1039+ topMargin: units.gu(0.5)
1040+ }
1041+
1042+ iconColor: "grey"
1043+ iconName: audioPlayer.playing ? "media-playback-pause" : "media-playback-start"
1044+
1045+ textSize: FontUtils.sizeToPixels("x-small")
1046+ text: {
1047+ if (audioPlayer.playing) {
1048+ return DateUtils.formattedTime(audioPlayer.position/ 1000)
1049+ }
1050+ return DateUtils.formattedTime(playbackBar.duration / 1000)
1051+ }
1052+
1053+ onClicked: {
1054+ if (audioPlayer.playing) {
1055+ audioPlayer.pause()
1056+ } else {
1057+ audioPlayer.play()
1058+ }
1059+ }
1060+ }
1061+
1062+ Slider {
1063+ id: slider
1064+ Connections {
1065+ target: audioPlayer
1066+ onDurationChanged: {
1067+ if (slider.maximumValue == 100) {
1068+ slider.maximumValue = audioPlayer.duration
1069+ }
1070+ }
1071+ }
1072+ style: SliderStyle {
1073+ Component.onCompleted: thumb.visible = false
1074+ Connections {
1075+ target: audioPlayer
1076+ onPlaybackStateChanged: {
1077+ thumb.visible = !audioPlayer.stopped
1078+ if (!thumb.visible) {
1079+ audioPlayer.seek(0)
1080+ }
1081+ }
1082+ }
1083+ }
1084+ enabled: !audioPlayer.stopped
1085+ function formatValue(v) { return DateUtils.formattedTime(v/1000) }
1086+ anchors {
1087+ top: parent.top
1088+ bottom: parent.bottom
1089+ left: playButton.right
1090+ right: parent.right
1091+ leftMargin: units.gu(1)
1092+ }
1093+ height: units.gu(3)
1094+ minimumValue: 0.0
1095+ maximumValue: 100
1096+ value: audioPlayer.position
1097+ activeFocusOnPress: false
1098+ onPressedChanged: {
1099+ if (!pressed) {
1100+ if (audioPlayer.playing || audioPlayer.paused) {
1101+ audioPlayer.seek(value)
1102+ } else {
1103+ audioPlayer.muted = true
1104+ // we only get the duration while playing
1105+ audioPlayer.play()
1106+ audioPlayer.pause()
1107+ if (audioPlayer.duration == 100) {
1108+ audioPlayer.seek((audioPlayer.duration*value)/100)
1109+ } else {
1110+ audioPlayer.seek(value)
1111+ }
1112+ audioPlayer.muted = false
1113+
1114+ }
1115+ value = Qt.binding(function(){ return audioPlayer.position})
1116+ }
1117+ }
1118+ }
1119+ }
1120+
1121+
1122+}
1123+
1124
1125=== added file 'src/qml/AudioRecordingBar.qml'
1126--- src/qml/AudioRecordingBar.qml 1970-01-01 00:00:00 +0000
1127+++ src/qml/AudioRecordingBar.qml 2016-01-14 12:41:18 +0000
1128@@ -0,0 +1,137 @@
1129+/*
1130+ * Copyright 2015 Canonical Ltd.
1131+ *
1132+ * This file is part of messaging-app.
1133+ *
1134+ * messaging-app is free software; you can redistribute it and/or modify
1135+ * it under the terms of the GNU General Public License as published by
1136+ * the Free Software Foundation; version 3.
1137+ *
1138+ * messaging-app is distributed in the hope that it will be useful,
1139+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1140+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1141+ * GNU General Public License for more details.
1142+ *
1143+ * You should have received a copy of the GNU General Public License
1144+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1145+ */
1146+
1147+import QtQuick 2.0
1148+import Ubuntu.Components 1.3
1149+import messagingapp.private 0.1
1150+import "dateUtils.js" as DateUtils
1151+
1152+Item {
1153+ id: recordingBar
1154+ opacity: audioRecorder.recording ? 1.0 : 0.0
1155+ Behavior on opacity { UbuntuNumberAnimation {} }
1156+ visible: opacity > 0
1157+
1158+ property int duration: 0
1159+ readonly property bool recording: audioRecorder.recording
1160+ property real buttonOpacity: 1
1161+
1162+ signal audioRecorded(var audio)
1163+
1164+ function startRecording() {
1165+ audioRecorder.record()
1166+ }
1167+
1168+ function stopRecording() {
1169+ audioRecorder.stop()
1170+ }
1171+
1172+ Loader {
1173+ id: audioRecorder
1174+ readonly property bool ready: status == Loader.Ready
1175+ readonly property bool recording: ready ? item.recorderState == AudioRecorder.RecordingState : false
1176+ readonly property int duration: ready ? item.duration : 0
1177+ function record() {
1178+ audioRecorder.active = true
1179+ item.record()
1180+ }
1181+ function stop() {
1182+ item.stop()
1183+ audioRecorder.active = false
1184+ }
1185+
1186+ active: false
1187+ sourceComponent: audioRecorderComponent
1188+ }
1189+
1190+ Component {
1191+ id: audioRecorderComponent
1192+ AudioRecorder {
1193+ readonly property bool recording: recorderState == AudioRecorder.RecordingState
1194+
1195+ onRecorderStateChanged: {
1196+ if (recorderState == AudioRecorder.StoppedState && actualLocation != "") {
1197+ var filePath = actualLocation
1198+
1199+ if (application.fileMimeType(filePath).toLowerCase().indexOf("audio/") <= -1) {
1200+ //If the recording process is too quick the generated file is not an audio one and should be ignored
1201+ return;
1202+ }
1203+
1204+ var attachment = {}
1205+ attachment["contentType"] = application.fileMimeType(filePath)
1206+ attachment["name"] = filePath.split('/').reverse()[0]
1207+ attachment["filePath"] = filePath
1208+ recordingBar.audioRecorded(attachment)
1209+
1210+ recordingBar.duration = duration
1211+ }
1212+ }
1213+
1214+ codec: "audio/vorbis"
1215+ quality: AudioRecorder.VeryHighQuality
1216+ }
1217+ }
1218+
1219+ TransparentButton {
1220+ id: recordingIcon
1221+ objectName: "recordingIcon"
1222+ iconPulsate: true
1223+ sideBySide: true
1224+ spacing: units.gu(1)
1225+ opacity: buttonOpacity
1226+
1227+ anchors {
1228+ left: parent.left
1229+ leftMargin: units.gu(2)
1230+ verticalCenter: parent.verticalCenter
1231+ }
1232+
1233+ focus: false
1234+
1235+ iconColor: "red"
1236+ iconName: "audio-input-microphone-symbolic"
1237+
1238+ textSize: FontUtils.sizeToPixels("x-small")
1239+ text: {
1240+ if (audioRecorder.recording) {
1241+ return DateUtils.formattedTime(audioRecorder.duration / 1000)
1242+ }
1243+ return DateUtils.formattedTime(0)
1244+ }
1245+ }
1246+
1247+ Label {
1248+ anchors {
1249+ top: parent.top
1250+ bottom: parent.bottom
1251+ left: recordingIcon.right
1252+ right: parent.right
1253+ topMargin: units.gu(1)
1254+ bottomMargin: units.gu(1)
1255+ leftMargin: units.gu(1)
1256+ rightMargin: units.gu(1)
1257+ }
1258+ opacity: buttonOpacity
1259+
1260+ text: i18n.tr("<<< Swipe to cancel")
1261+ verticalAlignment: Text.AlignVCenter
1262+ horizontalAlignment: Text.AlignHCenter
1263+ }
1264+}
1265+
1266
1267=== modified file 'src/qml/CMakeLists.txt'
1268--- src/qml/CMakeLists.txt 2014-08-11 22:22:59 +0000
1269+++ src/qml/CMakeLists.txt 2016-01-14 12:41:18 +0000
1270@@ -17,3 +17,4 @@
1271
1272 add_subdirectory(MMS)
1273 add_subdirectory(Dialogs)
1274+add_subdirectory(Stickers)
1275
1276=== added file 'src/qml/ComposeBar.qml'
1277--- src/qml/ComposeBar.qml 1970-01-01 00:00:00 +0000
1278+++ src/qml/ComposeBar.qml 2016-01-14 12:41:18 +0000
1279@@ -0,0 +1,534 @@
1280+/*
1281+ * Copyright 2012-2015 Canonical Ltd.
1282+ *
1283+ * This file is part of messaging-app.
1284+ *
1285+ * messaging-app is free software; you can redistribute it and/or modify
1286+ * it under the terms of the GNU General Public License as published by
1287+ * the Free Software Foundation; version 3.
1288+ *
1289+ * messaging-app is distributed in the hope that it will be useful,
1290+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1291+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1292+ * GNU General Public License for more details.
1293+ *
1294+ * You should have received a copy of the GNU General Public License
1295+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1296+ */
1297+
1298+import QtQuick 2.0
1299+import QtMultimedia 5.0
1300+import Ubuntu.Components 1.3
1301+import Ubuntu.Components.ListItems 1.3 as ListItem
1302+import Ubuntu.Components.Popups 1.3
1303+import Ubuntu.Content 0.1
1304+import Ubuntu.Telephony 0.1
1305+import "Stickers"
1306+
1307+Item {
1308+ id: composeBar
1309+
1310+ property bool showContents: true
1311+ property int maxHeight: textEntry.height + units.gu(2)
1312+ property variant attachments: []
1313+ property bool canSend: true
1314+ property alias text: messageTextArea.text
1315+ property bool audioAttached: attachments.count == 1 && attachments.get(0).contentType.toLowerCase().indexOf("audio/") > -1
1316+ // Audio QML component needs to process the recorded audio to find duration and AudioRecorder seems to erase duration after some events
1317+ property alias audioRecordedDuration: audioRecordingBar.duration
1318+ property alias recording: audioRecordingBar.recording
1319+ property bool oskEnabled: true
1320+
1321+ signal sendRequested(string text, var attachments)
1322+
1323+ // internal properties
1324+ property int _activeAttachmentIndex: -1
1325+ property int _defaultHeight: textEntry.height + attachmentPanel.height + stickersPicker.height + units.gu(2)
1326+
1327+ Component.onDestruction: {
1328+ composeBar.reset()
1329+ }
1330+
1331+ function forceFocus() {
1332+ messageTextArea.forceActiveFocus()
1333+ }
1334+
1335+ function reset() {
1336+ if (composeBar.audioAttached) {
1337+ FileOperations.remove(attachments.get(0).filePath)
1338+ }
1339+
1340+ textEntry.text = ""
1341+ attachments.clear()
1342+ }
1343+
1344+ function addAttachments(transfer) {
1345+ for (var i = 0; i < transfer.items.length; i++) {
1346+ if (String(transfer.items[i].text).length > 0) {
1347+ composeBar.text = String(transfer.items[i].text)
1348+ continue
1349+ }
1350+ var attachment = {}
1351+ if (!startsWith(String(transfer.items[i].url),"file://")) {
1352+ composeBar.text = String(transfer.items[i].url)
1353+ continue
1354+ }
1355+ var filePath = String(transfer.items[i].url).replace('file://', '')
1356+ // get only the basename
1357+ attachment["contentType"] = application.fileMimeType(filePath)
1358+ if (startsWith(attachment["contentType"], "text/vcard") ||
1359+ startsWith(attachment["contentType"], "text/x-vcard")) {
1360+ attachment["name"] = "contact.vcf"
1361+ } else {
1362+ attachment["name"] = filePath.split('/').reverse()[0]
1363+ }
1364+ attachment["filePath"] = filePath
1365+ attachments.append(attachment)
1366+ }
1367+ }
1368+
1369+ anchors.bottom: isSearching ? parent.bottom : keyboard.top
1370+ anchors.left: parent.left
1371+ anchors.right: parent.right
1372+ height: showContents ? Math.min(_defaultHeight, maxHeight) : 0
1373+ visible: showContents
1374+ clip: true
1375+
1376+ Behavior on height {
1377+ UbuntuNumberAnimation { }
1378+ }
1379+
1380+ MouseArea {
1381+ enabled: !composeBar.audioAttached
1382+ anchors.fill: parent
1383+ onClicked: {
1384+ forceFocus()
1385+ }
1386+ }
1387+
1388+ ListModel {
1389+ id: attachments
1390+ }
1391+
1392+ Component {
1393+ id: attachmentPopover
1394+
1395+ Popover {
1396+ id: popover
1397+ Column {
1398+ id: containerLayout
1399+ anchors {
1400+ left: parent.left
1401+ top: parent.top
1402+ right: parent.right
1403+ }
1404+ ListItem.Standard {
1405+ text: i18n.tr("Remove")
1406+ onClicked: {
1407+ attachments.remove(_activeAttachmentIndex)
1408+ PopupUtils.close(popover)
1409+ }
1410+ }
1411+ }
1412+ Component.onDestruction: _activeAttachmentIndex = -1
1413+ }
1414+ }
1415+
1416+ ListItem.ThinDivider {
1417+ anchors.top: parent.top
1418+ }
1419+
1420+ Row {
1421+ id: leftSideActions
1422+ opacity: {
1423+ if (composeBar.recording) {
1424+ // we need to fade the buttons in when dragging
1425+ return dragTarget.dragAmount
1426+ } else if (composeBar.audioAttached) {
1427+ return 0;
1428+ } else {
1429+ return 1
1430+ }
1431+ }
1432+
1433+ Behavior on opacity { UbuntuNumberAnimation {} }
1434+ visible: opacity > 0
1435+
1436+ width: childrenRect.width
1437+ height: childrenRect.height
1438+
1439+ anchors {
1440+ left: parent.left
1441+ leftMargin: units.gu(2)
1442+ verticalCenter: sendButton.verticalCenter
1443+ }
1444+ spacing: units.gu(2)
1445+
1446+ TransparentButton {
1447+ id: attachButton
1448+ objectName: "attachButton"
1449+ iconName: "add"
1450+ iconRotation: attachmentPanel.expanded ? 45 : 0
1451+ onClicked: {
1452+ attachmentPanel.expanded = !attachmentPanel.expanded
1453+ if (attachmentPanel.expanded) {
1454+ stickersPicker.expanded = false
1455+ }
1456+ }
1457+ }
1458+
1459+ TransparentButton {
1460+ id: stickersButton
1461+ objectName: "stickersButton"
1462+ iconSource: (stickersPicker.expanded && oskEnabled) ? Qt.resolvedUrl("./assets/input-keyboard-symbolic.svg") :
1463+ Qt.resolvedUrl("./assets/face-smile-big-symbolic-2.svg")
1464+ visible: stickerPacksModel.count > 0
1465+ onClicked: {
1466+ if (!stickersPicker.expanded) {
1467+ messageTextArea.focus = false
1468+ stickersPicker.expanded = true
1469+ attachmentPanel.expanded = false
1470+ } else {
1471+ stickersPicker.expanded = false
1472+ messageTextArea.forceActiveFocus()
1473+ }
1474+ }
1475+ }
1476+ }
1477+
1478+ AudioPlaybackBar {
1479+ id: audioPlaybackBar
1480+
1481+ anchors {
1482+ left: parent.left
1483+ right: audioRecordingBar.right
1484+ top: parent.top
1485+ bottom: attachmentPanel.top
1486+ }
1487+
1488+ source: composeBar.audioAttached ? attachments.get(0).filePath : ""
1489+ duration: audioRecordedDuration
1490+
1491+ opacity: composeBar.audioAttached ? 1.0 : 0.0
1492+ Behavior on opacity { UbuntuNumberAnimation {} }
1493+ visible: opacity > 0
1494+
1495+ onResetRequested: {
1496+ composeBar.reset()
1497+ }
1498+ }
1499+
1500+ AudioRecordingBar {
1501+ id: audioRecordingBar
1502+
1503+ anchors {
1504+ left: parent.left
1505+ right: dragTarget.left
1506+ top: parent.top
1507+ bottom: attachmentPanel.top
1508+ }
1509+
1510+ buttonOpacity: 1 - dragTarget.dragAmount
1511+
1512+ onAudioRecorded: {
1513+ attachments.append(audio)
1514+ }
1515+ }
1516+
1517+ Item {
1518+ id: dragTarget
1519+
1520+ property real recordingX: recordButton.x
1521+ property real normalX: leftSideActions.x + leftSideActions.width
1522+ property real delta: recordingX - normalX
1523+ property real dragAmount: 1 - (x - normalX) / (delta > 0 ? delta : 0.0001)
1524+ x: (composeBar.recording || composeBar.audioAttached) ? recordingX : normalX
1525+ Behavior on x { UbuntuNumberAnimation { } }
1526+ width: 0
1527+ }
1528+
1529+ StyledItem {
1530+ id: textEntry
1531+ property alias text: messageTextArea.text
1532+ property alias inputMethodComposing: messageTextArea.inputMethodComposing
1533+ property int fullSize: composeBar.audioAttached ? messageTextArea.height : attachmentThumbnails.height + messageTextArea.height
1534+ style: Theme.createStyleComponent("TextAreaStyle.qml", textEntry)
1535+ anchors {
1536+ topMargin: units.gu(1)
1537+ top: parent.top
1538+ left: dragTarget.right
1539+ leftMargin: units.gu(2)
1540+ right: sendButton.left
1541+ rightMargin: units.gu(2)
1542+ }
1543+ height: attachments.count !== 0 && !composeBar.audioAttached ? fullSize + units.gu(1.5) : fullSize
1544+ onActiveFocusChanged: {
1545+ if(activeFocus) {
1546+ stickersPicker.expanded = false
1547+ messageTextArea.forceActiveFocus()
1548+ } else {
1549+ focus = false
1550+ }
1551+ }
1552+
1553+ onTextChanged: {
1554+ // in case there is audio attached and the user starts typing, we remove the attachment
1555+ // and continue the text message
1556+ if (text !== "" && composeBar.audioAttached) {
1557+ attachments.clear()
1558+ }
1559+ }
1560+
1561+ focus: false
1562+ opacity: composeBar.audioAttached ? 0.0 : 1.0
1563+ Behavior on opacity { UbuntuNumberAnimation {} }
1564+ MouseArea {
1565+ anchors.fill: parent
1566+ onClicked: forceFocus()
1567+ }
1568+ Flow {
1569+ id: attachmentThumbnails
1570+ spacing: units.gu(1)
1571+ anchors{
1572+ left: parent.left
1573+ right: parent.right
1574+ top: parent.top
1575+ topMargin: units.gu(1)
1576+ leftMargin: units.gu(1)
1577+ rightMargin: units.gu(1)
1578+ }
1579+ height: childrenRect.height
1580+
1581+ Repeater {
1582+ model: attachments
1583+ delegate: Loader {
1584+ id: loader
1585+ height: units.gu(8)
1586+ source: {
1587+ var contentType = getContentType(filePath)
1588+ console.log(contentType)
1589+ switch(contentType) {
1590+ case ContentType.Contacts:
1591+ return Qt.resolvedUrl("ThumbnailContact.qml")
1592+ case ContentType.Pictures:
1593+ return Qt.resolvedUrl("ThumbnailImage.qml")
1594+ case ContentType.Videos:
1595+ return Qt.resolvedUrl("ThumbnailVideo.qml")
1596+ case ContentType.Unknown:
1597+ return Qt.resolvedUrl("ThumbnailUnknown.qml")
1598+ default:
1599+ console.log("unknown content Type")
1600+ }
1601+ }
1602+ onStatusChanged: {
1603+ if (status == Loader.Ready) {
1604+ item.index = index
1605+ item.filePath = filePath
1606+ }
1607+ }
1608+
1609+ Connections {
1610+ target: loader.status == Loader.Ready ? loader.item : null
1611+ ignoreUnknownSignals: true
1612+ onPressAndHold: {
1613+ Qt.inputMethod.hide()
1614+ _activeAttachmentIndex = target.index
1615+ PopupUtils.open(attachmentPopover, parent)
1616+ }
1617+ }
1618+ }
1619+ }
1620+ }
1621+
1622+ ListItem.ThinDivider {
1623+ id: divider
1624+
1625+ anchors {
1626+ left: parent.left
1627+ right: parent.right
1628+ top: attachmentThumbnails.bottom
1629+ margins: units.gu(0.5)
1630+ }
1631+ visible: attachments.count > 0
1632+ }
1633+
1634+ TextArea {
1635+ id: messageTextArea
1636+ objectName: "messageTextArea"
1637+ anchors {
1638+ top: attachments.count == 0 ? textEntry.top : attachmentThumbnails.bottom
1639+ left: parent.left
1640+ right: parent.right
1641+ }
1642+ // this value is to avoid letter being cut off
1643+ height: units.gu(4.3)
1644+ style: LocalTextAreaStyle {}
1645+ autoSize: true
1646+ maximumLineCount: attachments.count == 0 ? 8 : 4
1647+ placeholderText: {
1648+ if (telepathyHelper.ready) {
1649+ var account = telepathyHelper.accountForId(presenceRequest.accountId)
1650+ if (account &&
1651+ (presenceRequest.type != PresenceRequest.PresenceTypeUnknown &&
1652+ presenceRequest.type != PresenceRequest.PresenceTypeUnset) &&
1653+ account.protocolInfo.serviceName !== "") {
1654+ console.log(presenceRequest.accountId)
1655+ console.log(presenceRequest.type)
1656+ return account.protocolInfo.serviceName
1657+ }
1658+ }
1659+ return i18n.tr("Write a message...")
1660+ }
1661+ focus: textEntry.focus
1662+ font.family: "Ubuntu"
1663+ font.pixelSize: FontUtils.sizeToPixels("medium")
1664+ color: "#5d5d5d"
1665+ }
1666+ }
1667+
1668+ AttachmentPanel {
1669+ id: attachmentPanel
1670+
1671+ anchors {
1672+ left: parent.left
1673+ right: parent.right
1674+ top: textEntry.bottom
1675+ topMargin: units.gu(1)
1676+ }
1677+
1678+ Connections {
1679+ target: composeBar
1680+ onAudioAttachedChanged: {
1681+ if (composeBar.audioAttached) {
1682+ attachmentPanel.expanded = false;
1683+ }
1684+ }
1685+ }
1686+
1687+ onAttachmentAvailable: {
1688+ attachments.append(attachment)
1689+ forceFocus()
1690+ }
1691+
1692+ onExpandedChanged: {
1693+ if (expanded && Qt.inputMethod.visible) {
1694+ attachmentPanel.forceActiveFocus()
1695+ }
1696+ }
1697+ }
1698+
1699+ Loader {
1700+ id: stickersPicker
1701+ property bool expanded: false
1702+ height: expanded ? item.height : 0
1703+ active: false
1704+ sourceComponent: stickersPickerComponent
1705+ anchors {
1706+ left: parent.left
1707+ right: parent.right
1708+ top: textEntry.bottom
1709+ }
1710+ onExpandedChanged: {
1711+ if (expanded) {
1712+ stickersPicker.active = expanded
1713+ }
1714+ if (active) {
1715+ item.expanded = expanded
1716+ }
1717+ }
1718+ }
1719+
1720+ Component {
1721+ id: stickersPickerComponent
1722+ StickersPicker {
1723+ id: stickersPicker1
1724+
1725+ onExpandedChanged: {
1726+ if (expanded && Qt.inputMethod.visible) {
1727+ stickersPicker1.forceActiveFocus()
1728+ }
1729+ }
1730+
1731+ onStickerSelected: {
1732+ if (!canSend) {
1733+ // FIXME: show a dialog saying what we need to do to be able to send
1734+ return
1735+ }
1736+
1737+ var attachment = {}
1738+ var filePath = String(path).replace('file://', '')
1739+ attachment["contentType"] = application.fileMimeType(filePath)
1740+ attachment["name"] = filePath.split('/').reverse()[0]
1741+ attachment["filePath"] = filePath
1742+
1743+ // we need to append the attachment to a ListModel, so create it dynamically
1744+ var attachments = Qt.createQmlObject("import QtQuick 2.0; ListModel { }", composeBar)
1745+ attachments.append(attachment)
1746+ composeBar.sendRequested("", attachments)
1747+ stickersPicker.expanded = false
1748+ }
1749+ }
1750+ }
1751+
1752+ TransparentButton {
1753+ id: sendButton
1754+ objectName: "sendButton"
1755+ anchors.verticalCenter: textEntry.verticalCenter
1756+ anchors.right: parent.right
1757+ anchors.rightMargin: units.gu(2)
1758+ iconSource: Qt.resolvedUrl("./assets/send.svg")
1759+ enabled: (canSend && (textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0))
1760+ opacity: textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0 ? 1.0 : 0.0
1761+ Behavior on opacity { UbuntuNumberAnimation {} }
1762+ visible: opacity > 0
1763+
1764+ onClicked: {
1765+ // make sure we flush everything we have prepared in the OSK preedit
1766+ Qt.inputMethod.commit();
1767+ if (textEntry.text == "" && attachments.count == 0) {
1768+ return
1769+ }
1770+
1771+ if (composeBar.audioAttached) {
1772+ textEntry.text = ""
1773+ }
1774+
1775+ composeBar.sendRequested(textEntry.text, attachments)
1776+ }
1777+ }
1778+
1779+ TransparentButton {
1780+ id: recordButton
1781+ objectName: "recordButton"
1782+
1783+ anchors {
1784+ verticalCenter: textEntry.verticalCenter
1785+ right: parent.right
1786+ rightMargin: units.gu(2)
1787+ }
1788+
1789+ opacity: textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0 ? 0.0 : 1.0
1790+ Behavior on opacity { UbuntuNumberAnimation {} }
1791+ visible: opacity > 0
1792+
1793+ iconColor: composeBar.recording ? "black" : "gray"
1794+ iconName: "audio-input-microphone-symbolic"
1795+
1796+ onPressed: audioRecordingBar.startRecording()
1797+ onReleased: {
1798+ audioRecordingBar.stopRecording()
1799+
1800+ // if dragged past the threshold, cancel
1801+ if (dragTarget.dragAmount >= 0.5) {
1802+ composeBar.reset()
1803+ }
1804+ }
1805+
1806+ // drag-to-cancel
1807+ drag.target: dragTarget
1808+ drag.axis: Drag.XAxis
1809+ drag.minimumX: (leftSideActions.x + leftSideActions.width)
1810+ drag.maximumX: recordButton.x
1811+
1812+ }
1813+}
1814
1815=== renamed file 'src/qml/PictureImport.qml' => 'src/qml/ContentImport.qml'
1816--- src/qml/PictureImport.qml 2015-09-14 13:51:27 +0000
1817+++ src/qml/ContentImport.qml 2016-01-14 12:41:18 +0000
1818@@ -24,17 +24,33 @@
1819
1820 property var importDialog: null
1821
1822- signal pictureReceived(string pictureUrl)
1823+ signal contentReceived(string contentUrl)
1824
1825- function requestNewPicture()
1826- {
1827+ function requestContent(contentType) {
1828 if (!root.importDialog) {
1829 root.importDialog = PopupUtils.open(contentHubDialog, root)
1830+ root.importDialog.contentType = contentType
1831 } else {
1832 console.warn("Import dialog already running")
1833 }
1834 }
1835
1836+ function requestPicture() {
1837+ requestContent(ContentHub.ContentType.Pictures)
1838+ }
1839+
1840+ function requestVideo() {
1841+ requestContent(ContentHub.ContentType.Videos)
1842+ }
1843+
1844+ function requestContact() {
1845+ requestContent(ContentHub.ContentType.Contacts)
1846+ }
1847+
1848+ function requestDocument() {
1849+ requestContent(ContentHub.ContentType.Documents)
1850+ }
1851+
1852 Component {
1853 id: contentHubDialog
1854
1855@@ -42,6 +58,7 @@
1856 id: dialogue
1857
1858 property alias activeTransfer: signalConnections.target
1859+ property alias contentType: peerPicker.contentType
1860
1861 focus: true
1862 Rectangle {
1863@@ -76,7 +93,7 @@
1864 if (dialogue.activeTransfer.state === ContentHub.ContentTransfer.Charged) {
1865 dialogue.hide()
1866 if (dialogue.activeTransfer.items.length > 0) {
1867- root.pictureReceived(dialogue.activeTransfer.items[0].url)
1868+ root.contentReceived(dialogue.activeTransfer.items[0].url)
1869 }
1870 }
1871
1872
1873=== added file 'src/qml/DeliveryStatus.qml'
1874--- src/qml/DeliveryStatus.qml 1970-01-01 00:00:00 +0000
1875+++ src/qml/DeliveryStatus.qml 2016-01-14 12:41:18 +0000
1876@@ -0,0 +1,38 @@
1877+/*
1878+ * Copyright 2015 Canonical Ltd.
1879+ *
1880+ * This file is part of messaging-app.
1881+ *
1882+ * messaging-app is free software; you can redistribute it and/or modify
1883+ * it under the terms of the GNU General Public License as published by
1884+ * the Free Software Foundation; version 3.
1885+ *
1886+ * messaging-app is distributed in the hope that it will be useful,
1887+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1888+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1889+ * GNU General Public License for more details.
1890+ *
1891+ * You should have received a copy of the GNU General Public License
1892+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1893+ */
1894+import QtQuick 2.2
1895+import Ubuntu.History 0.1
1896+
1897+Image {
1898+ property int messageStatus: -1
1899+ enabled: true
1900+ height: enabled ? units.gu(1) : 0
1901+ width: enabled ? undefined : 0
1902+ fillMode: Image.PreserveAspectFit
1903+ source: {
1904+ if (!enabled) {
1905+ return ""
1906+ }
1907+ if (messageStatus == HistoryThreadModel.MessageStatusDelivered) {
1908+ return Qt.resolvedUrl("./assets/single_tick.svg")
1909+ } else if (messageStatus == HistoryThreadModel.MessageStatusRead) {
1910+ return Qt.resolvedUrl("./assets/double_tick.svg")
1911+ }
1912+ return ""
1913+ }
1914+}
1915
1916=== added file 'src/qml/Dialogs/FileSizeWarningDialog.qml'
1917--- src/qml/Dialogs/FileSizeWarningDialog.qml 1970-01-01 00:00:00 +0000
1918+++ src/qml/Dialogs/FileSizeWarningDialog.qml 2016-01-14 12:41:18 +0000
1919@@ -0,0 +1,70 @@
1920+/*
1921+ * Copyright 2015 Canonical Ltd.
1922+ *
1923+ * This file is part of messaging-app.
1924+ *
1925+ * dialer-app is free software; you can redistribute it and/or modify
1926+ * it under the terms of the GNU General Public License as published by
1927+ * the Free Software Foundation; version 3.
1928+ *
1929+ * dialer-app is distributed in the hope that it will be useful,
1930+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1931+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1932+ * GNU General Public License for more details.
1933+ *
1934+ * You should have received a copy of the GNU General Public License
1935+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1936+ */
1937+
1938+import QtQuick 2.0
1939+import Ubuntu.Components 1.3
1940+import Ubuntu.Components.Popups 1.3
1941+
1942+Component {
1943+ Dialog {
1944+ id: dialogue
1945+ title: i18n.tr("File size warning")
1946+ Column {
1947+ anchors.left: parent.left
1948+ anchors.right: parent.right
1949+ spacing: units.gu(2)
1950+
1951+ Label {
1952+ anchors.left: parent.left
1953+ anchors.right: parent.right
1954+ height: paintedHeight
1955+ verticalAlignment: Text.AlignVCenter
1956+ text: i18n.tr("You are trying to send big files (over 300Kb). Some operators might not be able to send it.")
1957+ wrapMode: Text.WordWrap
1958+ }
1959+ Row {
1960+ spacing: units.gu(4)
1961+ anchors.horizontalCenter: parent.horizontalCenter
1962+ Button {
1963+ objectName: "okFileSizeWarningDialog"
1964+ text: i18n.tr("Ok")
1965+ color: UbuntuColors.orange
1966+ onClicked: {
1967+ PopupUtils.close(dialogue)
1968+ }
1969+ }
1970+ }
1971+
1972+ Row {
1973+ CheckBox {
1974+ id: dontAskAgainCheckbox
1975+ checked: false
1976+ onCheckedChanged: settings.messagesDontShowFileSizeWarning = checked
1977+ }
1978+ Label {
1979+ text: i18n.tr("Don't show again")
1980+ anchors.verticalCenter: dontAskAgainCheckbox.verticalCenter
1981+ MouseArea {
1982+ anchors.fill: parent
1983+ onClicked: dontAskAgainCheckbox.checked = !dontAskAgainCheckbox.checked
1984+ }
1985+ }
1986+ }
1987+ }
1988+ }
1989+}
1990
1991=== modified file 'src/qml/KeyboardRectangle.qml'
1992--- src/qml/KeyboardRectangle.qml 2014-08-11 22:59:14 +0000
1993+++ src/qml/KeyboardRectangle.qml 2016-01-14 12:41:18 +0000
1994@@ -17,6 +17,7 @@
1995 */
1996
1997 import QtQuick 2.2
1998+import GSettings 1.0
1999
2000 Item {
2001 id: keyboardRect
2002@@ -25,6 +26,8 @@
2003 anchors.bottom: parent.bottom
2004 height: Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0
2005
2006+ property bool oskEnabled: !gsettings.stayHidden
2007+
2008 function recursiveFindFocusedItem(parent) {
2009 if (parent.activeFocus) {
2010 return parent;
2011@@ -58,4 +61,9 @@
2012 }
2013 }
2014 }
2015+
2016+ GSettings {
2017+ id: gsettings
2018+ schema.id: "com.canonical.keyboard.maliit"
2019+ }
2020 }
2021
2022=== modified file 'src/qml/LocalPageWithBottomEdge.qml'
2023--- src/qml/LocalPageWithBottomEdge.qml 2015-09-14 13:51:27 +0000
2024+++ src/qml/LocalPageWithBottomEdge.qml 2016-01-14 12:41:18 +0000
2025@@ -105,7 +105,7 @@
2026 {
2027 if (edgeLoader.status === Loader.Ready) {
2028 edgeLoader.item.active = true
2029- page.pageStack.push(edgeLoader.item)
2030+ page.pageStack.addPageToNextColumn(page, edgeLoader.item)
2031 if (edgeLoader.item.flickable) {
2032 edgeLoader.item.flickable.contentY = -page.header.height
2033 edgeLoader.item.flickable.returnToBounds()
2034@@ -162,6 +162,7 @@
2035 property bool hidden: (activeFocus === false) || ((bottomEdge.y - units.gu(1)) < tip.y)
2036
2037 enabled: mouseArea.enabled
2038+ visible: bottomEdgeEnabled
2039 anchors {
2040 bottom: parent.bottom
2041 horizontalCenter: bottomEdge.horizontalCenter
2042@@ -233,7 +234,7 @@
2043 minimumY: bottomEdge.pageStartY
2044 maximumY: page.height
2045 }
2046- enabled: edgeLoader.status == Loader.Ready
2047+ enabled: bottomEdgeEnabled && edgeLoader.status == Loader.Ready
2048
2049 anchors {
2050 left: parent.left
2051
2052=== added file 'src/qml/MMS/MMSAudio.qml'
2053--- src/qml/MMS/MMSAudio.qml 1970-01-01 00:00:00 +0000
2054+++ src/qml/MMS/MMSAudio.qml 2016-01-14 12:41:18 +0000
2055@@ -0,0 +1,200 @@
2056+/*
2057+ * Copyright 2015 Canonical Ltd.
2058+ *
2059+ * This file is part of messaging-app.
2060+ *
2061+ * messaging-app is free software; you can redistribute it and/or modify
2062+ * it under the terms of the GNU General Public License as published by
2063+ * the Free Software Foundation; version 3.
2064+ *
2065+ * messaging-app is distributed in the hope that it will be useful,
2066+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
2067+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
2068+ * GNU General Public License for more details.
2069+ *
2070+ * You should have received a copy of the GNU General Public License
2071+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
2072+ */
2073+
2074+import QtQuick 2.2
2075+import QtMultimedia 5.0
2076+import Ubuntu.Components 1.3
2077+import Ubuntu.Components.Themes.Ambiance 1.3
2078+import messagingapp.private 0.1
2079+import ".."
2080+import "../dateUtils.js" as DateUtils
2081+
2082+MMSBase {
2083+ id: audioDelegate
2084+
2085+ height: units.gu(5)
2086+ width: units.gu(28)
2087+ property string textColor: incoming ? "#5D5D5D" : "#FFFFFF"
2088+ swipeLocked: audioPlayer.playing
2089+
2090+ onAttachmentChanged: {
2091+ var tmpFile = FileOperations.getTemporaryFile(".ogg")
2092+ if (FileOperations.link(attachment.filePath, tmpFile)) {
2093+ audioPlayer.source = tmpFile;
2094+ } else {
2095+ console.log("MMSAudio: Failed to link", attachment.filePath, "to", tmpFile)
2096+ }
2097+ }
2098+
2099+ Rectangle {
2100+ id: shape
2101+ radius: units.gu(1)
2102+ smooth: true
2103+ anchors.top: parent.top
2104+ width: parent.width
2105+ height: parent.height
2106+ color: incoming ? "#FFFFFF" : "#3fb24f"
2107+ border.color: incoming ? "#888888" : "transparent"
2108+ }
2109+
2110+ Loader {
2111+ id: audioPlayer
2112+ readonly property bool playing: ready ? item.playing : false
2113+ readonly property bool paused: ready ? item.paused : false
2114+ readonly property bool stopped: ready ? item.stopped : false
2115+ readonly property int position: ready ? item.position : 0
2116+ readonly property int duration: ready ? item.duration : 0
2117+ readonly property bool ready: status == Loader.Ready
2118+ readonly property int playbackState: ready ? item.playbackState : Audio.StoppedState
2119+ property bool muted: false
2120+ property string source: ""
2121+ function play() {
2122+ audioPlayer.active = true
2123+ item.play()
2124+ }
2125+ function stop() {
2126+ item.stop()
2127+ audioPlayer.active = false
2128+ audioPlayer.muted = false
2129+ }
2130+ function pause() { item.pause() }
2131+ function seek(pos) { item.seek(pos) }
2132+ active: false
2133+ sourceComponent: audioPlayerComponent
2134+ }
2135+
2136+ Component {
2137+ id: audioPlayerComponent
2138+ Audio {
2139+ id: audioPlayer1
2140+ objectName: "audioPlayer"
2141+ readonly property bool playing: audioPlayer1.playbackState == Audio.PlayingState
2142+ readonly property bool paused: audioPlayer1.playbackState == Audio.PausedState
2143+ readonly property bool stopped: audioPlayer1.playbackState == Audio.StoppedState
2144+ source: audioPlayer.source
2145+ muted: audioPlayer.muted
2146+ }
2147+ }
2148+
2149+
2150+ TransparentButton {
2151+ id: playButton
2152+ objectName: "playButton"
2153+
2154+ anchors {
2155+ left: parent.left
2156+ leftMargin: units.gu(1)
2157+ verticalCenter: shape.verticalCenter
2158+ }
2159+
2160+ spacing: units.gu(1)
2161+ sideBySide: true
2162+ enabled: audioPlayer.source != ""
2163+ iconColor: audioDelegate.textColor
2164+ iconName: audioPlayer.playing ? "media-playback-pause" : "media-playback-start"
2165+
2166+ textSize: FontUtils.sizeToPixels("x-small")
2167+ textColor: audioDelegate.textColor
2168+ text: {
2169+ if (audioPlayer.playing || audioPlayer.paused) {
2170+ return DateUtils.formattedTime(audioPlayer.position/ 1000)
2171+ }
2172+ if (audioPlayer.duration > 0) {
2173+ return DateUtils.formattedTime(audioPlayer.duration / 1000)
2174+ }
2175+ return ""
2176+ }
2177+
2178+ onClicked: {
2179+ if (audioPlayer.playing) {
2180+ audioPlayer.pause()
2181+ } else {
2182+ audioPlayer.play()
2183+ }
2184+ }
2185+ }
2186+
2187+ Slider {
2188+ id: slider
2189+ Connections {
2190+ target: audioPlayer
2191+ onDurationChanged: {
2192+ if (slider.maximumValue == 100) {
2193+ slider.maximumValue = audioPlayer.duration
2194+ }
2195+ }
2196+ }
2197+ style: SliderStyle {
2198+ Component.onCompleted: thumb.visible = false
2199+ Connections {
2200+ target: audioPlayer
2201+ onPlaybackStateChanged: {
2202+ thumb.visible = !audioPlayer.stopped
2203+ if (!thumb.visible) {
2204+ audioPlayer.seek(0)
2205+ }
2206+ }
2207+ }
2208+ }
2209+ enabled: !audioPlayer.stopped
2210+ function formatValue(v) { return DateUtils.formattedTime(v/1000) }
2211+ anchors {
2212+ left: playButton.right
2213+ right: deliveryStatus.left
2214+ leftMargin: units.gu(1)
2215+ rightMargin: units.gu(2)
2216+ verticalCenter: shape.verticalCenter
2217+ }
2218+ height: units.gu(3)
2219+ minimumValue: 0.0
2220+ maximumValue: 100
2221+ value: audioPlayer.position
2222+ activeFocusOnPress: false
2223+ onPressedChanged: {
2224+ if (!pressed) {
2225+ if (audioPlayer.playing || audioPlayer.paused) {
2226+ audioPlayer.seek(value)
2227+ } else {
2228+ audioPlayer.muted = true
2229+ // we only get the duration while playing
2230+ audioPlayer.play()
2231+ audioPlayer.pause()
2232+ if (audioPlayer.duration == 100) {
2233+ audioPlayer.seek((audioPlayer.duration*value)/100)
2234+ } else {
2235+ audioPlayer.seek(value)
2236+ }
2237+ audioPlayer.muted = false
2238+
2239+ }
2240+ value = Qt.binding(function(){ return audioPlayer.position})
2241+ }
2242+ }
2243+ }
2244+
2245+ DeliveryStatus {
2246+ id: deliveryStatus
2247+ messageStatus: textMessageStatus
2248+ enabled: showDeliveryStatus
2249+ anchors {
2250+ right: parent.right
2251+ rightMargin: units.gu(0.5)
2252+ verticalCenter: slider.verticalCenter
2253+ }
2254+ }
2255+}
2256
2257=== modified file 'src/qml/MMS/MMSBase.qml'
2258--- src/qml/MMS/MMSBase.qml 2014-08-19 18:41:57 +0000
2259+++ src/qml/MMS/MMSBase.qml 2016-01-14 12:41:18 +0000
2260@@ -23,4 +23,6 @@
2261 property var attachment
2262 property string previewer
2263 property bool lastItem: false
2264+ property bool swipeLocked: false
2265+ property bool showDeliveryStatus: true
2266 }
2267
2268=== modified file 'src/qml/MMS/MMSContact.qml'
2269--- src/qml/MMS/MMSContact.qml 2015-11-19 13:37:36 +0000
2270+++ src/qml/MMS/MMSContact.qml 2016-01-14 12:41:18 +0000
2271@@ -20,6 +20,7 @@
2272 import Ubuntu.Components 1.3
2273 import Ubuntu.Contacts 0.1
2274 import Ubuntu.History 0.1
2275+import ".."
2276
2277 MMSBase {
2278 id: vcardDelegate
2279@@ -76,8 +77,8 @@
2280 return "#3fb24f"
2281 }
2282 }
2283- border.color: "#ACACAC"
2284- radius: height * 0.1
2285+ border.color: incoming ? "#ACACAC" : "transparent"
2286+ radius: units.gu(1)
2287
2288 ContactAvatar {
2289 id: avatar
2290@@ -132,4 +133,16 @@
2291
2292 vCardUrl: attachment ? Qt.resolvedUrl(attachment.filePath) : ""
2293 }
2294+
2295+ DeliveryStatus {
2296+ id: deliveryStatus
2297+ messageStatus: textMessageStatus
2298+ enabled: showDeliveryStatus
2299+ anchors {
2300+ right: parent.right
2301+ rightMargin: units.gu(0.5)
2302+ bottom: parent.bottom
2303+ bottomMargin: units.gu(0.5)
2304+ }
2305+ }
2306 }
2307
2308=== modified file 'src/qml/MMS/MMSImage.qml'
2309--- src/qml/MMS/MMSImage.qml 2015-09-14 13:51:27 +0000
2310+++ src/qml/MMS/MMSImage.qml 2016-01-14 12:41:18 +0000
2311@@ -18,6 +18,7 @@
2312
2313 import QtQuick 2.2
2314 import Ubuntu.Components 1.3
2315+import ".."
2316
2317 MMSBase {
2318 id: imageDelegate
2319@@ -34,6 +35,7 @@
2320
2321 image: Image {
2322 id: imageAttachment
2323+ objectName: "imageAttachment"
2324
2325 fillMode: Image.PreserveAspectCrop
2326 smooth: true
2327@@ -83,4 +85,16 @@
2328 }
2329 }
2330 }
2331+
2332+ DeliveryStatus {
2333+ id: deliveryStatus
2334+ messageStatus: textMessageStatus
2335+ enabled: showDeliveryStatus
2336+ anchors {
2337+ right: parent.right
2338+ rightMargin: units.gu(0.5)
2339+ bottom: parent.bottom
2340+ bottomMargin: units.gu(0.5)
2341+ }
2342+ }
2343 }
2344
2345=== modified file 'src/qml/MMS/MMSVideo.qml'
2346--- src/qml/MMS/MMSVideo.qml 2015-09-14 13:51:27 +0000
2347+++ src/qml/MMS/MMSVideo.qml 2016-01-14 12:41:18 +0000
2348@@ -1,5 +1,5 @@
2349 /*
2350- * Copyright 2012, 2013, 2014 Canonical Ltd.
2351+ * Copyright 2012-2015 Canonical Ltd.
2352 *
2353 * This file is part of messaging-app.
2354 *
2355@@ -18,65 +18,95 @@
2356
2357 import QtQuick 2.2
2358 import Ubuntu.Components 1.3
2359-import QtMultimedia 5.0
2360+import Ubuntu.Thumbnailer 0.1
2361 import ".."
2362
2363 MMSBase {
2364 id: videoDelegate
2365
2366 previewer: "MMS/PreviewerVideo.qml"
2367- anchors.left: parent.left
2368- anchors.right: parent.right
2369- height: bubble.height + units.gu(1)
2370+ height: videoAttachment.height
2371+ width: videoAttachment.width
2372
2373- Item {
2374+ UbuntuShape {
2375 id: bubble
2376 anchors.top: parent.top
2377- width: videoOutput.width + units.gu(3)
2378- height: videoOutput.height + units.gu(2)
2379-
2380- MediaPlayer {
2381- id: video
2382- autoLoad: true
2383- autoPlay: false
2384- source: attachment.filePath
2385- onStatusChanged: {
2386- if (status === MediaPlayer.Loaded) {
2387- // FIXME: there is no way to show the thumbnail
2388- video.play(); video.stop();
2389-
2390- // resize videoOutput, as width is not set
2391- // properly when using PreserveAspectFit
2392- if (videoOutput.height > units.gu(25)) {
2393- var percentageResized = units.gu(25)*100/(metaData.resolution.height)
2394- videoOutput.height = units.gu(25)
2395- videoOutput.width = (metaData.resolution.width*percentageResized)/100
2396- }
2397- if (videoOutput.width > units.gu(35)) {
2398- percentageResized = units.gu(35)*100/(metaData.resolution.width)
2399- videoOutput.width = units.gu(35)
2400- videoOutput.height = (metaData.resolution.height*percentageResized)/100
2401- }
2402+ width: image.width
2403+ height: image.height
2404+
2405+ image: Image {
2406+ id: videoAttachment
2407+ objectName: "videoAttachment"
2408+
2409+ fillMode: Image.PreserveAspectCrop
2410+ smooth: true
2411+ source: "image://thumbnailer/" + attachment.filePath
2412+ visible: false
2413+ asynchronous: true
2414+ height: Math.min(implicitHeight, units.gu(14))
2415+ width: Math.min(implicitWidth, units.gu(27))
2416+ cache: false
2417+
2418+ sourceSize.width: units.gu(27)
2419+ sourceSize.height: units.gu(27)
2420+
2421+ onStatusChanged: {
2422+ if (status === Image.Error) {
2423+ source = "image://theme/image-missing"
2424+ width = 128
2425+ height = 128
2426 }
2427 }
2428 }
2429- VideoOutput {
2430- id: videoOutput
2431- source: video
2432+
2433+ Icon {
2434+ objectName: "playbackStartIcon"
2435+ width: units.gu(3)
2436+ height: units.gu(3)
2437 anchors.centerIn: parent
2438- anchors.horizontalCenterOffset: incoming ? units.gu(0.5) : -units.gu(0.5)
2439+ name: "media-playback-start"
2440+ color: "white"
2441+ opacity: 0.8
2442 }
2443
2444 Rectangle {
2445- color: "black"
2446- opacity: 0.8
2447- anchors.fill: videoOutput
2448- Icon {
2449- name: "media-playback-start"
2450- width: units.gu(4)
2451- height: units.gu(4)
2452- anchors.centerIn: parent
2453+ visible: videoDelegate.lastItem
2454+ gradient: Gradient {
2455+ GradientStop { position: 0.0; color: "transparent" }
2456+ GradientStop { position: 1.0; color: "gray" }
2457+ }
2458+
2459+ anchors {
2460+ bottom: parent.bottom
2461+ left: parent.left
2462+ right: parent.right
2463+ }
2464+ height: units.gu(2)
2465+ radius: bubble.height * 0.1
2466+ Label {
2467+ anchors{
2468+ left: parent.left
2469+ bottom: parent.bottom
2470+ leftMargin: incoming ? units.gu(2) : units.gu(1)
2471+ bottomMargin: units.gu(0.5)
2472+ }
2473+ fontSize: "xx-small"
2474+ text: Qt.formatTime(timestamp).toLowerCase()
2475+ color: "white"
2476 }
2477 }
2478 }
2479+
2480+ DeliveryStatus {
2481+ id: deliveryStatus
2482+ messageStatus: textMessageStatus
2483+ enabled: showDeliveryStatus
2484+ anchors {
2485+ right: parent.right
2486+ rightMargin: units.gu(0.5)
2487+ bottom: parent.bottom
2488+ bottomMargin: units.gu(0.5)
2489+ }
2490+ }
2491+
2492 }
2493
2494=== modified file 'src/qml/MMS/Previewer.qml'
2495--- src/qml/MMS/Previewer.qml 2015-11-17 14:39:21 +0000
2496+++ src/qml/MMS/Previewer.qml 2016-01-14 12:41:18 +0000
2497@@ -109,11 +109,11 @@
2498
2499 onPeerSelected: {
2500 picker.curTransfer = peer.request();
2501- mainStack.pop();
2502+ mainStack.removePages(picker);
2503 if (picker.curTransfer.state === ContentTransfer.InProgress)
2504 picker.__exportItems(picker.url);
2505 }
2506- onCancelPressed: mainStack.pop();
2507+ onCancelPressed: mainStack.removePages(picker);
2508 }
2509
2510 Connections {
2511
2512=== modified file 'src/qml/MMS/PreviewerImage.qml'
2513--- src/qml/MMS/PreviewerImage.qml 2015-09-14 13:51:27 +0000
2514+++ src/qml/MMS/PreviewerImage.qml 2016-01-14 12:41:18 +0000
2515@@ -1,5 +1,5 @@
2516 /*
2517- * Copyright 2012, 2013, 2014 Canonical Ltd.
2518+ * Copyright 2012-2015 Canonical Ltd.
2519 *
2520 * This file is part of messaging-app.
2521 *
2522@@ -19,18 +19,193 @@
2523 import QtQuick 2.2
2524 import Ubuntu.Components 1.3
2525 import Ubuntu.Content 0.1
2526+import Ubuntu.Thumbnailer 0.1
2527 import ".."
2528
2529 Previewer {
2530+ id: imagePreviewer
2531+
2532+ Component.onCompleted: application.fullscreen = true
2533+ Component.onDestruction: application.fullscreen = false
2534+
2535+ Connections {
2536+ target: application
2537+ onFullscreenChanged: imagePreviewer.head.visible = !application.fullscreen
2538+ }
2539+
2540 title: i18n.tr("Image Preview")
2541 clip: true
2542- Image {
2543- anchors.centerIn: parent
2544+
2545+ Rectangle {
2546 anchors.fill: parent
2547- fillMode: Image.PreserveAspectFit
2548- source: attachment.filePath
2549- cache: false
2550- sourceSize.width: parent.width
2551- sourceSize.height: parent.height
2552+ color: "black"
2553+ }
2554+
2555+ Item {
2556+ id: imageItem
2557+ property bool pinchInProgress: zoomPinchArea.active
2558+ property size thumbSize: Qt.size(viewer.width * 1.05, viewer.height * 1.05)
2559+
2560+ onWidthChanged: {
2561+ // Only change thumbSize if width increases more than 5%
2562+ // that way we do not reload image for small resizes
2563+ if (width > thumbSize.width) {
2564+ thumbSize = Qt.size(width * 1.05, height * 1.05);
2565+ }
2566+ }
2567+
2568+ onHeightChanged: {
2569+ // Only change thumbSize if height increases more than 5%
2570+ // that way we do not reload image for small resizes
2571+ if (height > thumbSize.height) {
2572+ thumbSize = Qt.size(width * 1.05, height * 1.05);
2573+ }
2574+ }
2575+
2576+ function zoomIn(centerX, centerY, factor) {
2577+ flickable.scaleCenterX = centerX / (flickable.sizeScale * flickable.width);
2578+ flickable.scaleCenterY = centerY / (flickable.sizeScale * flickable.height);
2579+ flickable.sizeScale = factor;
2580+ }
2581+
2582+ function zoomOut() {
2583+ if (flickable.sizeScale != 1.0) {
2584+ flickable.scaleCenterX = flickable.contentX / flickable.width / (flickable.sizeScale - 1);
2585+ flickable.scaleCenterY = flickable.contentY / flickable.height / (flickable.sizeScale - 1);
2586+ flickable.sizeScale = 1.0;
2587+ }
2588+ }
2589+
2590+ width: parent.width
2591+ height: parent.height
2592+
2593+ ActivityIndicator {
2594+ objectName: "imageActivityIndicator"
2595+ anchors.centerIn: parent
2596+ visible: running
2597+ running: image.status != Image.Ready
2598+ }
2599+
2600+ PinchArea {
2601+ id: zoomPinchArea
2602+ anchors.fill: parent
2603+
2604+ property real initialZoom
2605+ property real maximumScale: 3.0
2606+ property real minimumZoom: 1.0
2607+ property real maximumZoom: 3.0
2608+ property bool active: false
2609+ property var center
2610+
2611+ onPinchStarted: {
2612+ active = true;
2613+ initialZoom = flickable.sizeScale;
2614+ center = zoomPinchArea.mapToItem(media, pinch.startCenter.x, pinch.startCenter.y);
2615+ imageItem.zoomIn(center.x, center.y, initialZoom);
2616+ }
2617+ onPinchUpdated: {
2618+ var zoomFactor = MathUtils.clamp(initialZoom * pinch.scale, minimumZoom, maximumZoom);
2619+ flickable.sizeScale = zoomFactor;
2620+ }
2621+ onPinchFinished: {
2622+ active = false;
2623+ }
2624+
2625+ Flickable {
2626+ id: flickable
2627+ anchors.fill: parent
2628+ contentWidth: media.width
2629+ contentHeight: media.height
2630+ contentX: (sizeScale - 1) * scaleCenterX * width
2631+ contentY: (sizeScale - 1) * scaleCenterY * height
2632+ interactive: !imageItem.pinchInProgress
2633+
2634+ property real sizeScale: 1.0
2635+ property real scaleCenterX: 0.0
2636+ property real scaleCenterY: 0.0
2637+
2638+ Behavior on sizeScale {
2639+ enabled: !imageItem.pinchInProgress
2640+ UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration}
2641+ }
2642+ Behavior on scaleCenterX {
2643+ UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration}
2644+ }
2645+ Behavior on scaleCenterY {
2646+ UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration}
2647+ }
2648+
2649+ Item {
2650+ id: media
2651+
2652+ width: flickable.width * flickable.sizeScale
2653+ height: flickable.height * flickable.sizeScale
2654+
2655+ Image {
2656+ id: image
2657+ objectName: "thumbnailImage"
2658+ anchors.fill: parent
2659+ asynchronous: true
2660+ cache: false
2661+ source: "image://thumbnailer/%1".arg(attachment.filePath.toString())
2662+ sourceSize {
2663+ width: imageItem.thumbSize.width
2664+ height: imageItem.thumbSize.height
2665+ }
2666+ fillMode: Image.PreserveAspectFit
2667+ opacity: status == Image.Ready ? 1.0 : 0.0
2668+ Behavior on opacity { UbuntuNumberAnimation {duration: UbuntuAnimation.FastDuration} }
2669+ }
2670+
2671+ Image {
2672+ id: highResolutionImage
2673+ objectName: "highResolutionImage"
2674+ anchors.fill: parent
2675+ asynchronous: true
2676+ cache: false
2677+ source: flickable.sizeScale > 1.0 ? attachment.filePath : ""
2678+ sourceSize {
2679+ width: width
2680+ height: height
2681+ }
2682+ fillMode: Image.PreserveAspectFit
2683+ }
2684+ }
2685+
2686+ MouseArea {
2687+ id: imageMouseArea
2688+ anchors.fill: parent
2689+
2690+ property bool clickAccepted: false
2691+
2692+ onDoubleClicked: {
2693+ if (imageMouseArea.clickAccepted) {
2694+ return
2695+ }
2696+
2697+ clickTimer.stop()
2698+
2699+ if (flickable.sizeScale < zoomPinchArea.maximumZoom) {
2700+ imageItem.zoomIn(mouse.x, mouse.y, zoomPinchArea.maximumZoom);
2701+ } else {
2702+ imageItem.zoomOut();
2703+ }
2704+ }
2705+ onClicked: {
2706+ imageMouseArea.clickAccepted = false
2707+ clickTimer.start()
2708+ }
2709+ }
2710+
2711+ Timer {
2712+ id: clickTimer
2713+ interval: 200
2714+ onTriggered: {
2715+ imageMouseArea.clickAccepted = true
2716+ application.fullscreen = !application.fullscreen
2717+ }
2718+ }
2719+ }
2720+ }
2721 }
2722 }
2723
2724=== modified file 'src/qml/MMS/PreviewerVideo.qml'
2725--- src/qml/MMS/PreviewerVideo.qml 2015-09-14 13:51:27 +0000
2726+++ src/qml/MMS/PreviewerVideo.qml 2016-01-14 12:41:18 +0000
2727@@ -1,5 +1,5 @@
2728 /*
2729- * Copyright 2012, 2013, 2014 Canonical Ltd.
2730+ * Copyright 2012-2015 Canonical Ltd.
2731 *
2732 * This file is part of messaging-app.
2733 *
2734@@ -17,61 +17,152 @@
2735 */
2736
2737 import QtQuick 2.2
2738+import QtMultimedia 5.0
2739 import Ubuntu.Components 1.3
2740-import QtMultimedia 5.0
2741+import Ubuntu.Content 0.1
2742+import Ubuntu.Thumbnailer 0.1
2743+import messagingapp.private 0.1
2744 import ".."
2745
2746 Previewer {
2747+ id: videoPreviewer
2748+
2749 title: i18n.tr("Video Preview")
2750- // This previewer implements only basic video controls: play/pause/rewind
2751- onActionTriggered: video.pause()
2752- MediaPlayer {
2753- id: video
2754- autoLoad: true
2755- autoPlay: true
2756- source: attachment.filePath
2757- }
2758- VideoOutput {
2759- id: videoOutput
2760- source: video
2761- anchors.fill: parent
2762+ clip: true
2763+
2764+ Component.onCompleted: {
2765+ application.fullscreen = true
2766+ // Load Video player after toggling fullscreen to reduce flickering
2767+ videoLoader.active = true
2768+ }
2769+ Component.onDestruction: application.fullscreen = false
2770+
2771+ Connections {
2772+ target: application
2773+ onFullscreenChanged: {
2774+ videoPreviewer.head.visible = !application.fullscreen
2775+ toolbar.collapsed = application.fullscreen
2776+ }
2777+ }
2778+
2779+ Rectangle {
2780+ anchors.fill: parent
2781+ color: "black"
2782+ }
2783+
2784+ Loader {
2785+ id: videoLoader
2786+
2787+ anchors.fill: parent
2788+ active: false
2789+ sourceComponent: videoComponent
2790+
2791+ onStatusChanged: {
2792+ if (status == Loader.Ready) {
2793+ var tmpFile = FileOperations.getTemporaryFile(".mp4")
2794+ if (FileOperations.link(attachment.filePath, tmpFile)) {
2795+ videoLoader.item.source = tmpFile
2796+ } else {
2797+ console.log("MMSVideo: Failed to link", attachment.filePath, "to", tmpFile)
2798+ }
2799+ }
2800+ }
2801+
2802+ Component {
2803+ id: videoComponent
2804+
2805+ Item {
2806+ id: videoPlayer
2807+ objectName: "videoPlayer"
2808+
2809+ property alias source: player.source
2810+ property alias playbackState: player.playbackState
2811+
2812+ function play() { player.play() }
2813+ function pause() { player.pause() }
2814+ function stop() { player.stop() }
2815+
2816+ anchors.fill: parent
2817+
2818+ MediaPlayer {
2819+ id: player
2820+ autoPlay: true
2821+ }
2822+
2823+ VideoOutput {
2824+ id: videoOutput
2825+ anchors.fill: parent
2826+ source: player
2827+ }
2828+ }
2829+ }
2830 }
2831
2832 MouseArea {
2833- id: playArea
2834- anchors.fill: parent
2835- onPressed: {
2836- if (video.playbackState === MediaPlayer.PlayingState) {
2837- video.pause()
2838- }
2839+ anchors {
2840+ top: parent.top
2841+ bottom: toolbar.top
2842+ left: parent.left
2843+ right: parent.right
2844 }
2845+ onClicked: application.fullscreen = !application.fullscreen
2846 }
2847
2848 Rectangle {
2849- color: "black"
2850- visible: video.playbackState !== MediaPlayer.PlayingState
2851+ id: toolbar
2852+ objectName: "toolbar"
2853+
2854+ property bool collapsed: false
2855+
2856+ anchors.bottom: parent.bottom
2857+
2858+ width: parent.width
2859+ height: collapsed ? 0 : units.gu(7)
2860+ Behavior on height { UbuntuNumberAnimation {} }
2861+
2862+ color: "gray"
2863 opacity: 0.8
2864- anchors.fill: videoOutput
2865+
2866 Row {
2867- anchors.centerIn: parent
2868+ anchors {
2869+ top: parent.top
2870+ bottom: parent.bottom
2871+ horizontalCenter: parent.horizontalCenter
2872+ }
2873+
2874+ spacing: units.gu(2)
2875+
2876 Icon {
2877- name: "media-playback-pause"
2878- width: units.gu(5)
2879- height: units.gu(5)
2880+ anchors.verticalCenter: parent.verticalCenter
2881+ width: toolbar.collapsed ? 0 : units.gu(5)
2882+ height: width
2883+ Behavior on width { UbuntuNumberAnimation {} }
2884+ Behavior on height { UbuntuNumberAnimation {} }
2885+ name: videoLoader.item && videoLoader.item.playbackState == MediaPlayer.PlayingState ? "media-playback-pause" : "media-playback-start"
2886+ color: "white"
2887 MouseArea {
2888 anchors.fill: parent
2889- onClicked: video.play();
2890+ onClicked: {
2891+ if (videoLoader.item.playbackState == MediaPlayer.PlayingState) {
2892+ videoLoader.item.pause()
2893+ } else {
2894+ videoLoader.item.play()
2895+ }
2896+ }
2897 }
2898 }
2899 Icon {
2900- name: "media-seek-backward"
2901- width: units.gu(5)
2902- height: units.gu(5)
2903+ anchors.verticalCenter: parent.verticalCenter
2904+ width: toolbar.collapsed ? 0 : units.gu(5)
2905+ height: width
2906+ Behavior on width { UbuntuNumberAnimation {} }
2907+ Behavior on height { UbuntuNumberAnimation {} }
2908+ name: "media-playback-stop"
2909+ color: "white"
2910 MouseArea {
2911 anchors.fill: parent
2912 onClicked: {
2913- video.stop();
2914- video.play();
2915+ videoLoader.item.stop()
2916 }
2917 }
2918 }
2919
2920=== modified file 'src/qml/MMSDelegate.qml'
2921--- src/qml/MMSDelegate.qml 2015-11-23 19:51:16 +0000
2922+++ src/qml/MMSDelegate.qml 2016-01-14 12:41:18 +0000
2923@@ -27,6 +27,15 @@
2924 property var dataAttachments: []
2925 property var textAttachements: []
2926 property string messageText: ""
2927+ swipeLocked: {
2928+ for (var i=0; i < attachmentsView.children.length; i++) {
2929+ if (attachmentsView.children[i].item && !attachmentsView.children[i].item.swipeLocked) {
2930+ return false
2931+ }
2932+ }
2933+ return true
2934+ }
2935+
2936
2937 function clicked(mouse)
2938 {
2939@@ -36,7 +45,7 @@
2940 var properties = {}
2941 properties["attachment"] = attachment.item.attachment
2942 properties["thumbnail"] = attachment.item
2943- mainStack.push(Qt.resolvedUrl(attachment.item.previewer), properties)
2944+ mainStack.addPageToCurrentColumn(messages, Qt.resolvedUrl(attachment.item.previewer), properties)
2945 }
2946 }
2947
2948@@ -82,17 +91,16 @@
2949 var attachment = attachments[i]
2950 if (startsWith(attachment.contentType, "text/plain") ) {
2951 root.textAttachements.push(attachment)
2952+ } else if (startsWith(attachment.contentType, "audio/")) {
2953+ root.dataAttachments.push({"type": "audio",
2954+ "data": attachment,
2955+ "delegateSource": "MMS/MMSAudio.qml",
2956+ })
2957 } else if (startsWith(attachment.contentType, "image/")) {
2958 root.dataAttachments.push({"type": "image",
2959 "data": attachment,
2960 "delegateSource": "MMS/MMSImage.qml",
2961 })
2962- //} else if (startsWith(attachment.contentType, "video/")) {
2963- // TODO: implement proper video attachment support
2964- // dataAttachments.push({type: "video",
2965- // data: attachment,
2966- // delegateSource: "MMS/MMSVideo.qml",
2967- // })
2968 } else if (startsWith(attachment.contentType, "application/smil") ||
2969 startsWith(attachment.contentType, "application/x-smil")) {
2970 // smil files will always be ignored here
2971@@ -102,6 +110,11 @@
2972 "data": attachment,
2973 "delegateSource": "MMS/MMSContact.qml"
2974 })
2975+ } else if (startsWith(attachment.contentType, "video/")) {
2976+ root.dataAttachments.push({"type": "video",
2977+ "data": attachment,
2978+ "delegateSource": "MMS/MMSVideo.qml",
2979+ })
2980 } else {
2981 root.dataAttachments.push({"type": "default",
2982 "data": attachment,
2983@@ -150,11 +163,6 @@
2984 target: attachmentLoader
2985 anchors.left: parent ? parent.left : undefined
2986 }
2987- PropertyChanges {
2988- target: attachmentLoader
2989- anchors.leftMargin: units.gu(1)
2990- anchors.rightMargin: 0
2991- }
2992 },
2993 State {
2994 when: !root.incoming
2995@@ -163,11 +171,6 @@
2996 target: attachmentLoader
2997 anchors.right: parent ? parent.right : undefined
2998 }
2999- PropertyChanges {
3000- target: attachmentLoader
3001- anchors.leftMargin: 0
3002- anchors.rightMargin: units.gu(1)
3003- }
3004 }
3005 ]
3006 source: modelData.delegateSource
3007@@ -221,7 +224,7 @@
3008 target: bubbleLoader.item
3009 property: "sender"
3010 value: messageData.sender.alias !== "" ? messageData.sender.alias : messageData.senderId
3011- when: participants.length > 1 && bubbleLoader.status === Loader.Ready && messageData.senderId !== "self"
3012+ when: messageData.participants.length > 1 && bubbleLoader.status === Loader.Ready && messageData.senderId !== "self"
3013 }
3014 }
3015 }
3016
3017=== modified file 'src/qml/MMSMessageBubble.qml'
3018--- src/qml/MMSMessageBubble.qml 2014-08-27 16:05:37 +0000
3019+++ src/qml/MMSMessageBubble.qml 2016-01-14 12:41:18 +0000
3020@@ -25,6 +25,7 @@
3021 messageStatus: messageData.textMessageStatus
3022 messageIncoming: incoming
3023 accountName: accountLabel
3024+ showDeliveryStatus: true
3025
3026 states: [
3027 State {
3028
3029=== modified file 'src/qml/MainPage.qml'
3030--- src/qml/MainPage.qml 2015-10-30 13:21:59 +0000
3031+++ src/qml/MainPage.qml 2016-01-14 12:41:18 +0000
3032@@ -33,11 +33,7 @@
3033 threadList.startSelection()
3034 }
3035
3036- state: selectionMode ? "select" : searching ? "search" : "default"
3037- title: selectionMode ? " " : i18n.tr("Messages")
3038- flickable: null
3039-
3040- bottomEdgeEnabled: !selectionMode && !searching
3041+ bottomEdgeEnabled: !selectionMode && !searching && !mainView.dualPanel
3042 bottomEdgeTitle: i18n.tr("+")
3043 bottomEdgePageComponent: Messages { active: false }
3044
3045@@ -46,6 +42,8 @@
3046 objectName: "searchField"
3047 visible: mainPage.searching
3048 anchors {
3049+ top: parent.top
3050+ topMargin: units.gu(1)
3051 left: parent.left
3052 right: parent.right
3053 rightMargin: units.gu(2)
3054@@ -60,11 +58,32 @@
3055 }
3056 }
3057
3058+ header: PageHeader {
3059+ id: pageHeader
3060+
3061+ property alias leadingActions: leadingBar.actions
3062+ property alias trailingActions: trailingBar.actions
3063+
3064+ onTrailingActionsChanged: console.log("Trailing actions lenght: " + trailingActions.length)
3065+
3066+ title: i18n.tr("Messages")
3067+ flickable: threadList
3068+ leadingActionBar {
3069+ id: leadingBar
3070+ }
3071+
3072+ trailingActionBar {
3073+ id: trailingBar
3074+ }
3075+ }
3076+
3077 states: [
3078- PageHeadState {
3079+ State {
3080+ id: defaultState
3081 name: "default"
3082- head: mainPage.head
3083- actions: [
3084+ when: !searching && !selectionMode
3085+
3086+ property list<QtObject> trailingActions: [
3087 Action {
3088 objectName: "searchAction"
3089 iconName: "search"
3090@@ -77,34 +96,55 @@
3091 objectName: "settingsAction"
3092 text: i18n.tr("Settings")
3093 iconName: "settings"
3094- onTriggered: pageStack.push(Qt.resolvedUrl("SettingsPage.qml"))
3095+ onTriggered: pageStack.addPageToNextColumn(mainPage, Qt.resolvedUrl("SettingsPage.qml"))
3096 }
3097 ]
3098+
3099+ PropertyChanges {
3100+ target: pageHeader
3101+ trailingActions: defaultState.trailingActions
3102+ leadingActions: []
3103+ }
3104 },
3105- PageHeadState {
3106+ State {
3107+ id: searchState
3108 name: "search"
3109- head: mainPage.head
3110- backAction: Action {
3111- objectName: "cancelSearch"
3112- visible: mainPage.searching
3113- iconName: "back"
3114- text: i18n.tr("Cancel")
3115- onTriggered: {
3116- searchField.text = ""
3117- mainPage.searching = false
3118+ when: searching
3119+
3120+ property list<QtObject> leadingActions: [
3121+ Action {
3122+ objectName: "cancelSearch"
3123+ visible: mainPage.searching
3124+ iconName: "back"
3125+ text: i18n.tr("Cancel")
3126+ onTriggered: {
3127+ searchField.text = ""
3128+ mainPage.searching = false
3129+ }
3130 }
3131+ ]
3132+
3133+ PropertyChanges {
3134+ target: pageHeader
3135+ contents: searchField
3136+ leadingActions: searchState.leadingActions
3137+ trailingActions: []
3138 }
3139- contents: searchField
3140 },
3141- PageHeadState {
3142- name: "select"
3143- head: mainPage.head
3144- backAction: Action {
3145- objectName: "selectionModeCancelAction"
3146- iconName: "back"
3147- onTriggered: threadList.cancelSelection()
3148- }
3149- actions: [
3150+ State {
3151+ id: selectionState
3152+ name: "selection"
3153+ when: selectionMode
3154+
3155+ property list<QtObject> leadingActions: [
3156+ Action {
3157+ objectName: "selectionModeCancelAction"
3158+ iconName: "back"
3159+ onTriggered: threadList.cancelSelection()
3160+ }
3161+ ]
3162+
3163+ property list<QtObject> trailingActions: [
3164 Action {
3165 objectName: "selectionModeSelectAllAction"
3166 iconName: "select"
3167@@ -117,6 +157,12 @@
3168 onTriggered: threadList.endSelection()
3169 }
3170 ]
3171+ PropertyChanges {
3172+ target: pageHeader
3173+ title: " "
3174+ leadingActions: selectionState.leadingActions
3175+ trailingActions: selectionState.trailingActions
3176+ }
3177 }
3178 ]
3179
3180@@ -218,10 +264,11 @@
3181 }
3182 properties["participantIds"] = participantIds
3183 properties["participants"] = model.participants
3184+ properties["presenceRequest"] = threadDelegate.presenceItem
3185 if (displayedEvent != null) {
3186 properties["scrollToEventId"] = displayedEvent.eventId
3187 }
3188- mainStack.push(Qt.resolvedUrl("Messages.qml"), properties)
3189+ mainStack.addPageToNextColumn(mainPage, Qt.resolvedUrl("Messages.qml"), properties)
3190 }
3191 }
3192 onItemPressAndHold: {
3193
3194=== modified file 'src/qml/MessageBubble.qml'
3195--- src/qml/MessageBubble.qml 2015-10-22 15:31:05 +0000
3196+++ src/qml/MessageBubble.qml 2016-01-14 12:41:18 +0000
3197@@ -24,7 +24,7 @@
3198 import "dateUtils.js" as DateUtils
3199 import "3rd_party/ba-linkify.js" as BaLinkify
3200
3201-Rectangle {
3202+BorderImage {
3203 id: root
3204
3205 property int messageStatus: -1
3206@@ -34,10 +34,15 @@
3207 property var messageTimeStamp
3208 property int maxDelegateWidth: units.gu(27)
3209 property string accountName
3210+ // FIXME for now we just display the delivery status if it's greater than Accepted
3211+ property bool showDeliveryStatus: false
3212+ property bool deliveryStatusAvailable: showDeliveryStatus && (statusDelivered || statusRead)
3213
3214 readonly property bool error: (messageStatus === HistoryThreadModel.MessageStatusPermanentlyFailed)
3215 readonly property bool sending: (messageStatus === HistoryThreadModel.MessageStatusUnknown ||
3216 messageStatus === HistoryThreadModel.MessageStatusTemporarilyFailed) && !messageIncoming
3217+ readonly property bool statusDelivered: (messageStatus === HistoryThreadModel.MessageStatusDelivered)
3218+ readonly property bool statusRead: (messageStatus === HistoryThreadModel.MessageStatusRead)
3219
3220 // XXXX: should be hoisted
3221 function getCountryCode() {
3222@@ -69,28 +74,33 @@
3223 return text
3224 }
3225
3226- color: {
3227+ property string color: {
3228 if (error) {
3229- return "#fc4949"
3230+ return "red"
3231 } else if (sending) {
3232- return "#b2b2b2"
3233+ return "grey"
3234 } else if (messageIncoming) {
3235- return "#ffffff"
3236+ return "white"
3237 } else {
3238- return "#3fb24f"
3239+ // FIXME: use blue for IM accounts
3240+ return "green"
3241 }
3242 }
3243- border.color: "#ACACAC"
3244-
3245- radius: units.gu(1)
3246- height: senderName.height + senderName.anchors.topMargin + textLabel.height + textTimestamp.height + units.gu(1)
3247+ source: "assets/" + color + "_bubble.sci"
3248+ smooth: true
3249+
3250+ // FIXME: maybe we should put everything inside a container to make width and height calculation easier
3251+ height: senderName.height + senderName.anchors.topMargin + textLabel.height + border.bottom + units.gu(0.5) + (oneLine ? 0 : messageFooter.height + messageFooter.anchors.topMargin)
3252+
3253+ // if possible, put the timestamp and the delivery status in the same line as the text
3254+ property int oneLineWidth: textLabel.contentWidth + messageFooter.width
3255+ property bool oneLine: oneLineWidth <= units.gu(27)
3256 width: Math.min(units.gu(27),
3257- Math.max(textLabel.contentWidth, textTimestamp.contentWidth, senderName.contentWidth))
3258+ Math.max(oneLine ? oneLineWidth : textLabel.contentWidth,
3259+ messageFooter.width,
3260+ senderName.contentWidth,
3261+ border.right + border.left - units.gu(3)))
3262 + units.gu(3)
3263- anchors{
3264- leftMargin: units.gu(1)
3265- rightMargin: units.gu(1)
3266- }
3267
3268 Label {
3269 id: senderName
3270@@ -126,73 +136,49 @@
3271 color: root.messageIncoming ? UbuntuColors.darkGrey : "white"
3272 }
3273
3274- Label {
3275- id: textTimestamp
3276- objectName: "messageDate"
3277+ Row {
3278+ id: messageFooter
3279+ width: childrenRect.width
3280+ spacing: units.gu(1)
3281
3282- anchors{
3283+ anchors {
3284 top: textLabel.bottom
3285- topMargin: units.gu(0.5)
3286- left: parent.left
3287- leftMargin: units.gu(1)
3288+ topMargin: oneLine ? -textTimestamp.height : units.gu(0.5)
3289+ right: parent.right
3290+ rightMargin: units.gu(1)
3291 }
3292
3293- visible: !root.sending
3294- height: units.gu(2)
3295- width: visible ? maxDelegateWidth : 0
3296- fontSize: "xx-small"
3297- color: root.messageIncoming ? UbuntuColors.lightGrey : "white"
3298- opacity: root.messageIncoming ? 1.0 : 0.8
3299- elide: Text.ElideRight
3300- text: {
3301- if (messageTimeStamp === "")
3302- return ""
3303-
3304- var str = Qt.formatTime(messageTimeStamp, Qt.DefaultLocaleShortDate)
3305- if (root.accountName.length === 0 || !root.messageIncoming) {
3306+ Label {
3307+ id: textTimestamp
3308+ objectName: "messageDate"
3309+
3310+ anchors.bottom: parent.bottom
3311+ visible: !root.sending
3312+ height: units.gu(2)
3313+ width: paintedWidth > maxDelegateWidth ? maxDelegateWidth : undefined
3314+ fontSize: "xx-small"
3315+ color: root.messageIncoming ? UbuntuColors.lightGrey : "white"
3316+ opacity: root.messageIncoming ? 1.0 : 0.8
3317+ elide: Text.ElideRight
3318+ verticalAlignment: Text.AlignVCenter
3319+ text: {
3320+ if (messageTimeStamp === "")
3321+ return ""
3322+
3323+ var str = Qt.formatTime(messageTimeStamp, Qt.DefaultLocaleShortDate)
3324+ if (root.accountName.length === 0 || !root.messageIncoming) {
3325+ return str
3326+ }
3327+ str += " @ %1".arg(root.accountName)
3328 return str
3329 }
3330- str += " @ %1".arg(root.accountName)
3331- return str
3332- }
3333- }
3334-
3335- ColoredImage {
3336- id: bubbleArrow
3337-
3338- source: Qt.resolvedUrl("./assets/conversation_bubble_arrow.png")
3339- color: root.color
3340- asynchronous: false
3341- anchors {
3342- bottom: parent.bottom
3343- bottomMargin: units.gu(2)
3344- leftMargin: -1
3345- rightMargin: -1
3346- }
3347- width: units.gu(1)
3348- height: units.gu(1.5)
3349-
3350- states: [
3351- State {
3352- when: root.messageIncoming
3353- name: "incoming"
3354- AnchorChanges {
3355- target: bubbleArrow
3356- anchors.right: root.left
3357- }
3358- },
3359- State {
3360- when: !root.messageIncoming
3361- name: "outgoing"
3362- AnchorChanges {
3363- target: bubbleArrow
3364- anchors.left: root.right
3365- }
3366- PropertyChanges {
3367- target: bubbleArrow
3368- mirror: true
3369- }
3370- }
3371- ]
3372+ }
3373+
3374+ DeliveryStatus {
3375+ id: deliveryStatus
3376+ messageStatus: messageStatus
3377+ enabled: deliveryStatusAvailable
3378+ anchors.verticalCenter: textTimestamp.verticalCenter
3379+ }
3380 }
3381 }
3382
3383=== modified file 'src/qml/MessageDelegate.qml'
3384--- src/qml/MessageDelegate.qml 2014-08-28 21:54:12 +0000
3385+++ src/qml/MessageDelegate.qml 2016-01-14 12:41:18 +0000
3386@@ -26,6 +26,7 @@
3387 property bool incoming: (messageData && messageData.senderId !== "self")
3388 property string accountLabel: ""
3389 property var _lastItem
3390+ property bool swipeLocked: false
3391
3392 function deleteMessage()
3393 {
3394
3395=== modified file 'src/qml/MessageDelegateFactory.qml'
3396--- src/qml/MessageDelegateFactory.qml 2015-09-14 13:51:27 +0000
3397+++ src/qml/MessageDelegateFactory.qml 2016-01-14 12:41:18 +0000
3398@@ -35,6 +35,8 @@
3399 signal resendMessage()
3400 signal copyMessage()
3401 signal showMessageDetails()
3402+ color: "transparent"
3403+ locked: loader.item.swipeLocked
3404
3405 width: messageList.width
3406 leftSideAction: Action {
3407
3408=== modified file 'src/qml/Messages.qml'
3409--- src/qml/Messages.qml 2015-12-07 17:55:17 +0000
3410+++ src/qml/Messages.qml 2016-01-14 12:41:18 +0000
3411@@ -1,5 +1,5 @@
3412 /*
3413- * Copyright 2012, 2013, 2014 Canonical Ltd.
3414+ * Copyright 2012-2015 Canonical Ltd.
3415 *
3416 * This file is part of messaging-app.
3417 *
3418@@ -25,6 +25,7 @@
3419 import Ubuntu.History 0.1
3420 import Ubuntu.Telephony 0.1
3421 import Ubuntu.Contacts 0.1
3422+import messagingapp.private 0.1
3423
3424 import "dateUtils.js" as DateUtils
3425
3426@@ -45,8 +46,6 @@
3427 // FIXME: MainView should provide if the view is in portait or landscape
3428 property int orientationAngle: Screen.angleBetween(Screen.primaryOrientation, Screen.orientation)
3429 property bool landscape: orientationAngle == 90 || orientationAngle == 270
3430- property var activeTransfer: null
3431- property int activeAttachmentIndex: -1
3432 property var sharedAttachmentsTransfer: []
3433 property alias contactWatcher: contactWatcherInternal
3434 property string text: ""
3435@@ -61,7 +60,16 @@
3436 property string firstParticipantId: participantIds.length > 0 ? participantIds[0] : ""
3437 property variant firstParticipant: participants.length > 0 ? participants[0] : null
3438 property var threads: []
3439+ property QtObject presenceRequest: presenceItem
3440 property var accountsModel: getAccountsModel()
3441+ property alias oskEnabled: keyboard.oskEnabled
3442+ property bool isReady: false
3443+ property string firstRecipientAlias: ((contactWatcher.isUnknown &&
3444+ contactWatcher.isInteractive) ||
3445+ contactWatcher.alias === "") ? contactWatcher.identifier : contactWatcher.alias
3446+
3447+ signal ready
3448+
3449 function getAccountsModel() {
3450 var accounts = []
3451 // on new chat dialogs display all possible accounts
3452@@ -179,123 +187,44 @@
3453 return (!tmpAccount || tmpAccount.type == AccountEntry.PhoneAccount || tmpAccount.type == AccountEntry.MultimediaAccount)
3454 }
3455
3456- Connections {
3457- target: telepathyHelper
3458- onSetupReady: {
3459- // force reevaluation
3460- messages.account = Qt.binding(getCurrentAccount)
3461- messages.phoneAccount = Qt.binding(isPhoneAccount)
3462- head.sections.model = Qt.binding(getSectionsModel)
3463- head.sections.selectedIndex = Qt.binding(getSelectedIndex)
3464- }
3465- }
3466-
3467-
3468- Connections {
3469- target: chatManager
3470- onChatEntryCreated: {
3471- // TODO: track using chatId and not participants
3472- if (accountId == account.accountId &&
3473- firstParticipant && participants[0] == firstParticipant.identifier) {
3474- messages.chatEntry = chatEntry
3475- }
3476- }
3477- onChatsChanged: {
3478- for (var i in chatManager.chats) {
3479- var chat = chatManager.chats[i]
3480- // TODO: track using chatId and not participants
3481- if (chat.account.accountId == account.accountId &&
3482- firstParticipant && chat.participants[0] == firstParticipant.identifier) {
3483- messages.chatEntry = chat
3484- return
3485- }
3486- }
3487- messages.chatEntry = null
3488- }
3489- }
3490-
3491- Timer {
3492- id: typingTimer
3493- interval: 6000
3494- onTriggered: {
3495- messages.userTyping = false;
3496- }
3497- }
3498-
3499- Repeater {
3500- model: messages.chatEntry ? messages.chatEntry.chatStates : null
3501- Item {
3502- function processChatState() {
3503- if (modelData.state == ChatEntry.ChannelChatStateComposing) {
3504- messages.userTyping = true
3505- typingTimer.start()
3506- } else {
3507- messages.userTyping = false
3508- }
3509- }
3510- Component.onCompleted: processChatState()
3511- Connections {
3512- target: modelData
3513- onStateChanged: processChatState()
3514- }
3515- }
3516- }
3517-
3518- MessagesHeader {
3519- id: header
3520- width: parent ? parent.width - units.gu(2) : undefined
3521- height: units.gu(5)
3522- title: messages.title
3523- subtitle: {
3524- if (userTyping) {
3525- return i18n.tr("Typing..")
3526- }
3527- switch (presenceRequest.type) {
3528- case PresenceRequest.PresenceTypeAvailable:
3529- return i18n.tr("Online")
3530- case PresenceRequest.PresenceTypeOffline:
3531- return i18n.tr("Offline")
3532- case PresenceRequest.PresenceTypeAway:
3533- return i18n.tr("Away")
3534- case PresenceRequest.PresenceTypeBusy:
3535- return i18n.tr("Busy")
3536- default:
3537- return ""
3538- }
3539- }
3540- visible: true
3541- }
3542-
3543- head {
3544- id: head
3545- sections.model: getSectionsModel()
3546- sections.selectedIndex: getSelectedIndex()
3547- }
3548-
3549-
3550- function addAttachmentsToModel(transfer) {
3551- for (var i in transfer.items) {
3552- if (String(transfer.items[i].text).length > 0) {
3553- messages.text = String(transfer.items[i].text)
3554- continue
3555- }
3556- var attachment = {}
3557- if (!startsWith(String(transfer.items[i].url),"file://")) {
3558- messages.text = String(transfer.items[i].url)
3559- continue
3560- }
3561- var filePath = String(transfer.items[i].url).replace('file://', '')
3562- // get only the basename
3563- attachment["contentType"] = application.fileMimeType(filePath)
3564- if (startsWith(attachment["contentType"], "text/vcard") ||
3565- startsWith(attachment["contentType"], "text/x-vcard")) {
3566- attachment["name"] = "contact.vcf"
3567- } else {
3568- attachment["name"] = filePath.split('/').reverse()[0]
3569- }
3570- attachment["filePath"] = filePath
3571- attachments.append(attachment)
3572- }
3573+ function addNewThreadToFilter(newAccountId, participantIds) {
3574+ var newAccount = telepathyHelper.accountForId(newAccountId)
3575+ var matchType = HistoryThreadModel.MatchCaseSensitive
3576+ if (newAccount.type == AccountEntry.PhoneAccount || newAccount.type == AccountEntry.MultimediaAccount) {
3577+ matchType = HistoryThreadModel.MatchPhoneNumber
3578+ }
3579+
3580+ var thread = eventModel.threadForParticipants(newAccountId,
3581+ HistoryThreadModel.EventTypeText,
3582+ participantIds,
3583+ matchType,
3584+ true)
3585+ var threadId = thread.threadId
3586+
3587+ // dont change the participants list
3588+ if (messages.participants.length == 0) {
3589+ messages.participants = thread.participants
3590+ var ids = []
3591+ for (var i in messages.participants) {
3592+ ids.push(messages.participants[i].identifier)
3593+ }
3594+ messages.participantIds = ids;
3595+ }
3596+
3597+ var found = false;
3598+ for (var i in messages.threads) {
3599+ if (messages.threads[i].threadId == threadId && messages.threads[i].accountId == newAccountId) {
3600+ found = true;
3601+ break;
3602+ }
3603+ }
3604+
3605+ if (!found) {
3606+ messages.threads.push({"accountId": newAccountId, "threadId": threadId})
3607+ reloadFilters = !reloadFilters
3608+ }
3609+
3610+ return thread
3611 }
3612
3613 function sendMessageNetworkCheck() {
3614@@ -340,48 +269,21 @@
3615 }
3616
3617 // create the new thread and update the threadId list
3618- var thread = eventModel.threadForParticipants(messages.account.accountId,
3619- HistoryThreadModel.EventTypeText,
3620- participantIds,
3621- messages.account.type == AccountEntry.PhoneAccount ? HistoryThreadModel.MatchPhoneNumber
3622- : HistoryThreadModel.MatchCaseSensitive,
3623- true)
3624- var threadId = thread.threadId
3625-
3626- // dont change the participants list
3627- if (messages.participants.length == 0) {
3628- messages.participants = thread.participants
3629- var ids = []
3630- for (var i in messages.participants) {
3631- ids.push(messages.participants[i].identifier)
3632- }
3633- messages.participantIds = ids;
3634- }
3635-
3636- var found = false;
3637- for (var i in messages.threads) {
3638- if (messages.threads[i].threadId == threadId && messages.threads[i].accountId == messages.account.accountId) {
3639- found = true;
3640- break;
3641- }
3642- }
3643- if (!found) {
3644- messages.threads.push({"accountId": messages.account.accountId, "threadId": threadId})
3645- reloadFilters = !reloadFilters
3646- }
3647+ var thread = addNewThreadToFilter(messages.account.accountId, participantIds)
3648+
3649 for (var i=0; i < eventModel.count; i++) {
3650 var event = eventModel.get(i)
3651 if (event.senderId == "self" && event.accountId != messages.account.accountId) {
3652 var tmpAccount = telepathyHelper.accountForId(event.accountId)
3653 if (!tmpAccount || (tmpAccount.type == AccountEntry.MultimediaAccount && messages.account.type == AccountEntry.PhoneAccount)) {
3654- // we don't add the information event if the last outgoing message
3655+ // we don't add the information event if the last outgoing message
3656 // was a fallback to a multimedia service
3657 break;
3658 }
3659 // if the last outgoing message used a different accountId, add an
3660 // information event and quit the loop
3661 eventModel.writeTextInformationEvent(messages.account.accountId,
3662- threadId,
3663+ thread.threadId,
3664 participantIds,
3665 "")
3666 break;
3667@@ -399,7 +301,7 @@
3668 var timestamp = new Date()
3669 var tmpEventId = timestamp.toISOString()
3670 event["accountId"] = messages.account.accountId
3671- event["threadId"] = threadId
3672+ event["threadId"] = thread.threadId
3673 event["eventId"] = tmpEventId
3674 event["type"] = HistoryEventModel.MessageTypeText
3675 event["participants"] = thread.participants
3676@@ -417,7 +319,7 @@
3677 var attachment = {}
3678 var item = attachments[i]
3679 attachment["accountId"] = messages.account.accountId
3680- attachment["threadId"] = threadId
3681+ attachment["threadId"] = thread.threadId
3682 attachment["eventId"] = tmpEventId
3683 attachment["attachmentId"] = item[0]
3684 attachment["contentType"] = item[1]
3685@@ -436,10 +338,14 @@
3686 var isSelfContactKnown = account.selfContactId != ""
3687 if (isMmsGroupChat && !isSelfContactKnown) {
3688 // TODO: inform the user to enter the phone number of the selected sim card manually
3689- // and use it in the telepathy-ofono account as selfContactId.
3690- return
3691- }
3692- chatManager.sendMessage(messages.account.accountId, participantIds, text, attachments, properties)
3693+ // and use it in the telepathy-ofono account as selfContactId.
3694+ return false
3695+ }
3696+ var fallbackAccountId = chatManager.sendMessage(messages.account.accountId, participantIds, text, attachments, properties)
3697+ // create the new thread and update the threadId list
3698+ if (fallbackAccountId != messages.account.accountId) {
3699+ addNewThreadToFilter(fallbackAccountId, participantIds)
3700+ }
3701 }
3702
3703 // FIXME: soon it won't be just about SIM cards, so the dialogs need updating
3704@@ -455,120 +361,6 @@
3705 return true
3706 }
3707
3708- PresenceRequest {
3709- id: presenceRequest
3710- accountId: {
3711- // if this is a regular sms chat, try requesting the presence on
3712- // a multimedia account
3713- if (!account) {
3714- return ""
3715- }
3716- if (account.type == AccountEntry.PhoneAccount) {
3717- for (var i in telepathyHelper.accounts) {
3718- var tmpAccount = telepathyHelper.accounts[i]
3719- if (tmpAccount.type == AccountEntry.MultimediaAccount) {
3720- return tmpAccount.accountId
3721- }
3722- }
3723- return ""
3724- }
3725- return account.accountId
3726- }
3727- // we just request presence on 1-1 chats
3728- identifier: participants.length == 1 ? participants[0].identifier : ""
3729- }
3730-
3731- // this is necessary to automatically update the view when the
3732- // default account changes in system settings
3733- Connections {
3734- target: mainView
3735- onAccountChanged: {
3736- if (!messages.phoneAccount) {
3737- return
3738- }
3739- messages.account = mainView.account
3740- }
3741- }
3742-
3743- ActivityIndicator {
3744- id: activityIndicator
3745- anchors {
3746- verticalCenter: parent.verticalCenter
3747- horizontalCenter: parent.horizontalCenter
3748- }
3749- running: isSearching
3750- visible: running
3751- }
3752-
3753- ListModel {
3754- id: attachments
3755- }
3756-
3757- PictureImport {
3758- id: pictureImporter
3759-
3760- onPictureReceived: {
3761- var attachment = {}
3762- var filePath = String(pictureUrl).replace('file://', '')
3763- attachment["contentType"] = application.fileMimeType(filePath)
3764- attachment["name"] = filePath.split('/').reverse()[0]
3765- attachment["filePath"] = filePath
3766- attachments.append(attachment)
3767- textEntry.forceActiveFocus()
3768- }
3769- }
3770-
3771- flickable: null
3772-
3773- property bool isReady: false
3774- signal ready
3775- onReady: {
3776- isReady = true
3777- if (participants.length === 0 && keyboardFocus)
3778- multiRecipient.forceFocus()
3779- }
3780-
3781- property string firstRecipientAlias: ((contactWatcher.isUnknown &&
3782- contactWatcher.isInteractive) ||
3783- contactWatcher.alias === "") ? contactWatcher.identifier : contactWatcher.alias
3784- title: {
3785- if (selectionMode || participants.length == 0) {
3786- return " "
3787- }
3788-
3789- if (landscape) {
3790- return ""
3791- }
3792- if (participants.length > 0) {
3793- if (participants.length == 1) {
3794- return firstRecipientAlias
3795- } else {
3796- // TRANSLATORS: %1 refers to the number of participants in a group chat
3797- return i18n.tr("Group (%1)").arg(participants.length)
3798- }
3799- }
3800- return i18n.tr("New Message")
3801- }
3802-
3803- Component.onCompleted: {
3804- if (messages.accountId !== "") {
3805- var account = telepathyHelper.accountForId(messages.accountId)
3806- if (account && account.type == AccountEntry.MultimediaAccount) {
3807- // fallback the first available phone account
3808- if (telepathyHelper.phoneAccounts.length > 0) {
3809- messages.accountId = telepathyHelper.phoneAccounts[0].accountId
3810- }
3811- }
3812- }
3813- addAttachmentsToModel(sharedAttachmentsTransfer)
3814- }
3815-
3816- onActiveChanged: {
3817- if (active && (eventModel.count > 0)){
3818- swipeItemDemo.enable()
3819- }
3820- }
3821-
3822 function updateFilters(accounts, participants, reload, threads) {
3823 if (participants.length == 0 || accounts.length == 0) {
3824 return null
3825@@ -584,7 +376,7 @@
3826 }
3827 return Qt.createQmlObject(componentUnion.arg(componentFilters), eventModel)
3828 }
3829-
3830+
3831 var filterAccounts = []
3832
3833 if (messages.accountsModel.length == 1 && messages.accountsModel[0].type == AccountEntry.GenericAccount) {
3834@@ -596,9 +388,9 @@
3835 filterAccounts.push(account)
3836 }
3837 }
3838- }
3839+ }
3840
3841- for (var i in filterAccounts) {
3842+ for (var i in filterAccounts) {
3843 var account = filterAccounts[i];
3844 var filterValue = eventModel.threadIdForParticipants(account.accountId,
3845 HistoryThreadModel.EventTypeText,
3846@@ -628,8 +420,283 @@
3847 return eventModel.markEventAsRead(accountId, threadId, eventId, type);
3848 }
3849
3850+ header: PageHeader {
3851+ id: pageHeader
3852+
3853+ property alias leadingActions: leadingBar.actions
3854+ property alias trailingActions: trailingBar.actions
3855+
3856+ title: {
3857+ if (landscape) {
3858+ return ""
3859+ }
3860+
3861+ if (participants.length == 1) {
3862+ return firstRecipientAlias
3863+ }
3864+
3865+ return i18n.tr("New Message")
3866+ }
3867+
3868+ Sections {
3869+ id: sections
3870+ anchors {
3871+ left: parent.left
3872+ leftMargin: units.gu(2)
3873+ bottom: parent.bottom
3874+ }
3875+ model: getSectionsModel()
3876+ selectedIndex: getSelectedIndex()
3877+ }
3878+
3879+ extension: sections.model.length > 1 ? sections : null
3880+
3881+ leadingActionBar {
3882+ id: leadingBar
3883+ }
3884+
3885+ trailingActionBar {
3886+ id: trailingBar
3887+ }
3888+ }
3889+
3890+ states: [
3891+ State {
3892+ id: selectionState
3893+ name: "selection"
3894+ when: selectionMode
3895+
3896+ property list<QtObject> leadingActions: [
3897+ Action {
3898+ objectName: "selectionModeCancelAction"
3899+ iconName: "back"
3900+ onTriggered: messageList.cancelSelection()
3901+ }
3902+ ]
3903+
3904+ property list<QtObject> trailingActions: [
3905+ Action {
3906+ objectName: "selectionModeSelectAllAction"
3907+ iconName: "select"
3908+ onTriggered: {
3909+ if (messageList.selectedItems.count === messageList.count) {
3910+ messageList.clearSelection()
3911+ } else {
3912+ messageList.selectAll()
3913+ }
3914+ }
3915+ },
3916+ Action {
3917+ objectName: "selectionModeDeleteAction"
3918+ enabled: messageList.selectedItems.count > 0
3919+ iconName: "delete"
3920+ onTriggered: messageList.endSelection()
3921+ }
3922+ ]
3923+
3924+ PropertyChanges {
3925+ target: pageHeader
3926+ title: " "
3927+ leadingActions: selectionState.leadingActions
3928+ trailingActions: selectionState.trailingActions
3929+ }
3930+ },
3931+ State {
3932+ id: groupChatState
3933+ name: "groupChat"
3934+ when: groupChat
3935+
3936+ property list<QtObject> trailingActions: [
3937+ Action {
3938+ objectName: "groupChatAction"
3939+ iconName: "contact-group"
3940+ onTriggered: PopupUtils.open(participantsPopover, screenTop)
3941+ }
3942+ ]
3943+
3944+ PropertyChanges {
3945+ target: pageHeader
3946+ // TRANSLATORS: %1 refers to the number of participants in a group chat
3947+ title: i18n.tr("Group (%1)").arg(participants.length)
3948+ contents: headerContents
3949+ trailingActions: groupChatState.trailingActions
3950+ }
3951+ },
3952+ State {
3953+ id: unknownContactState
3954+ name: "unknownContact"
3955+ when: participants.length == 1 && contactWatcher.isUnknown
3956+
3957+ property list<QtObject> trailingActions: [
3958+ Action {
3959+ objectName: "contactCallAction"
3960+ visible: participants.length == 1 && contactWatcher.interactive
3961+ iconName: "call-start"
3962+ text: i18n.tr("Call")
3963+ onTriggered: {
3964+ Qt.inputMethod.hide()
3965+ // FIXME: support other things than just phone numbers
3966+ Qt.openUrlExternally("tel:///" + encodeURIComponent(contactWatcher.identifier))
3967+ }
3968+ },
3969+ Action {
3970+ objectName: "addContactAction"
3971+ visible: contactWatcher.isUnknown && participants.length == 1 && contactWatcher.interactive
3972+ iconName: "contact-new"
3973+ text: i18n.tr("Add")
3974+ onTriggered: {
3975+ Qt.inputMethod.hide()
3976+ // FIXME: support other things than just phone numbers
3977+ mainView.addPhoneToContact(messages, "", contactWatcher.identifier, null, null)
3978+ }
3979+ }
3980+ ]
3981+ PropertyChanges {
3982+ target: pageHeader
3983+ contents: headerContents
3984+ trailingActions: unknownContactState.trailingActions
3985+ }
3986+ },
3987+ State {
3988+ id: newMessageState
3989+ name: "newMessage"
3990+ when: participants.length === 0
3991+ property list<QtObject> trailingActions: [
3992+ Action {
3993+ objectName: "contactList"
3994+ iconName: "contact"
3995+ onTriggered: {
3996+ Qt.inputMethod.hide()
3997+ mainStack.addPageToCurrentColumn(messages, Qt.resolvedUrl("NewRecipientPage.qml"), {"multiRecipient": multiRecipient, "parentPage": messages})
3998+ }
3999+ }
4000+
4001+ ]
4002+
4003+ property Item contents: MultiRecipientInput {
4004+ id: multiRecipient
4005+ objectName: "multiRecipient"
4006+ enabled: visible
4007+ anchors {
4008+ left: parent ? parent.left : undefined
4009+ right: parent ? parent.right : undefined
4010+ rightMargin: units.gu(2)
4011+ top: parent ? parent.top: undefined
4012+ topMargin: units.gu(1)
4013+ }
4014+ }
4015+
4016+ PropertyChanges {
4017+ target: pageHeader
4018+ title: " "
4019+ trailingActions: newMessageState.trailingActions
4020+ contents: newMessageState.contents
4021+ }
4022+ },
4023+ State {
4024+ id: knownContactState
4025+ name: "knownContact"
4026+ when: participants.length == 1 && !contactWatcher.isUnknown
4027+ property list<QtObject> trailingActions: [
4028+ Action {
4029+ objectName: "contactCallKnownAction"
4030+ visible: participants.length == 1 && messages.phoneAccount
4031+ iconName: "call-start"
4032+ text: i18n.tr("Call")
4033+ onTriggered: {
4034+ Qt.inputMethod.hide()
4035+ // FIXME: support other things than just phone numbers
4036+ Qt.openUrlExternally("tel:///" + encodeURIComponent(contactWatcher.identifier))
4037+ }
4038+ },
4039+ Action {
4040+ objectName: "contactProfileAction"
4041+ visible: !contactWatcher.isUnknown && participants.length == 1 && messages.phoneAccount
4042+ iconSource: "image://theme/contact"
4043+ text: i18n.tr("Contact")
4044+ onTriggered: {
4045+ mainView.showContactDetails(messages, contactWatcher.contactId, null, null)
4046+ }
4047+ }
4048+ ]
4049+ PropertyChanges {
4050+ target: pageHeader
4051+ contents: headerContents
4052+ trailingActions: knownContactState.trailingActions
4053+ }
4054+ }
4055+ ]
4056+
4057+ Component.onCompleted: {
4058+ if (messages.accountId !== "") {
4059+ var account = telepathyHelper.accountForId(messages.accountId)
4060+ if (account && account.type == AccountEntry.MultimediaAccount) {
4061+ // fallback the first available phone account
4062+ if (telepathyHelper.phoneAccounts.length > 0) {
4063+ messages.accountId = telepathyHelper.phoneAccounts[0].accountId
4064+ }
4065+ }
4066+ }
4067+ composeBar.addAttachments(sharedAttachmentsTransfer)
4068+ }
4069+
4070+ onReady: {
4071+ isReady = true
4072+ if (participants.length === 0 && keyboardFocus)
4073+ multiRecipient.forceFocus()
4074+ }
4075+
4076+ onActiveChanged: {
4077+ if (active && (eventModel.count > 0)){
4078+ swipeItemDemo.enable()
4079+ }
4080+ }
4081+
4082+ Connections {
4083+ target: telepathyHelper
4084+ onSetupReady: {
4085+ // force reevaluation
4086+ messages.account = Qt.binding(getCurrentAccount)
4087+ messages.phoneAccount = Qt.binding(isPhoneAccount)
4088+ head.sections.model = Qt.binding(getSectionsModel)
4089+ head.sections.selectedIndex = Qt.binding(getSelectedIndex)
4090+ }
4091+ }
4092+
4093+ Connections {
4094+ target: chatManager
4095+ onChatEntryCreated: {
4096+ // TODO: track using chatId and not participants
4097+ if (accountId == account.accountId &&
4098+ firstParticipant && participants[0] == firstParticipant.identifier) {
4099+ messages.chatEntry = chatEntry
4100+ }
4101+ }
4102+ onChatsChanged: {
4103+ for (var i in chatManager.chats) {
4104+ var chat = chatManager.chats[i]
4105+ // TODO: track using chatId and not participants
4106+ if (chat.account.accountId == account.accountId &&
4107+ firstParticipant && chat.participants[0] == firstParticipant.identifier) {
4108+ messages.chatEntry = chat
4109+ return
4110+ }
4111+ }
4112+ messages.chatEntry = null
4113+ }
4114+ }
4115+
4116+ // this is necessary to automatically update the view when the
4117+ // default account changes in system settings
4118 Connections {
4119 target: mainView
4120+ onAccountChanged: {
4121+ if (!messages.phoneAccount) {
4122+ return
4123+ }
4124+ messages.account = mainView.account
4125+ }
4126+
4127 onApplicationActiveChanged: {
4128 if (mainView.applicationActive) {
4129 for (var i in pendingEventsToMarkAsRead) {
4130@@ -641,28 +708,96 @@
4131 }
4132 }
4133
4134- Component {
4135- id: attachmentPopover
4136-
4137- Popover {
4138- id: popover
4139- Column {
4140- id: containerLayout
4141- anchors {
4142- left: parent.left
4143- top: parent.top
4144- right: parent.right
4145+ Connections {
4146+ target: messages.head.sections
4147+ onSelectedIndexChanged: {
4148+ messages.account = messages.accountsModel[head.sections.selectedIndex]
4149+ }
4150+ }
4151+
4152+ Timer {
4153+ id: typingTimer
4154+ interval: 6000
4155+ onTriggered: {
4156+ messages.userTyping = false;
4157+ }
4158+ }
4159+
4160+ Repeater {
4161+ model: messages.chatEntry ? messages.chatEntry.chatStates : null
4162+ Item {
4163+ function processChatState() {
4164+ if (modelData.state == ChatEntry.ChannelChatStateComposing) {
4165+ messages.userTyping = true
4166+ typingTimer.start()
4167+ } else {
4168+ messages.userTyping = false
4169 }
4170- ListItem.Standard {
4171- text: i18n.tr("Remove")
4172- onClicked: {
4173- attachments.remove(activeAttachmentIndex)
4174- PopupUtils.close(popover)
4175+ }
4176+ Component.onCompleted: processChatState()
4177+ Connections {
4178+ target: modelData
4179+ onStateChanged: processChatState()
4180+ }
4181+ }
4182+ }
4183+
4184+ MessagesHeader {
4185+ id: headerContents
4186+ width: parent ? parent.width - units.gu(2) : undefined
4187+ height: units.gu(5)
4188+ title: pageHeader.title
4189+ subtitle: {
4190+ if (userTyping) {
4191+ return i18n.tr("Typing..")
4192+ }
4193+ switch (presenceRequest.type) {
4194+ case PresenceRequest.PresenceTypeAvailable:
4195+ return i18n.tr("Online")
4196+ case PresenceRequest.PresenceTypeOffline:
4197+ return i18n.tr("Offline")
4198+ case PresenceRequest.PresenceTypeAway:
4199+ return i18n.tr("Away")
4200+ case PresenceRequest.PresenceTypeBusy:
4201+ return i18n.tr("Busy")
4202+ default:
4203+ return ""
4204+ }
4205+ }
4206+ visible: true
4207+ }
4208+
4209+ PresenceRequest {
4210+ id: presenceItem
4211+ accountId: {
4212+ // if this is a regular sms chat, try requesting the presence on
4213+ // a multimedia account
4214+ if (!account) {
4215+ return ""
4216+ }
4217+ if (account.type == AccountEntry.PhoneAccount) {
4218+ for (var i in telepathyHelper.accounts) {
4219+ var tmpAccount = telepathyHelper.accounts[i]
4220+ if (tmpAccount.type == AccountEntry.MultimediaAccount) {
4221+ return tmpAccount.accountId
4222 }
4223 }
4224+ return ""
4225 }
4226- Component.onDestruction: activeAttachmentIndex = -1
4227- }
4228+ return account.accountId
4229+ }
4230+ // we just request presence on 1-1 chats
4231+ identifier: participants.length == 1 ? participants[0].identifier : ""
4232+ }
4233+
4234+ ActivityIndicator {
4235+ id: activityIndicator
4236+ anchors {
4237+ verticalCenter: parent.verticalCenter
4238+ horizontalCenter: parent.horizontalCenter
4239+ }
4240+ running: isSearching
4241+ visible: running
4242 }
4243
4244 Component {
4245@@ -727,13 +862,6 @@
4246 }
4247 }
4248
4249- Connections {
4250- target: messages.head.sections
4251- onSelectedIndexChanged: {
4252- messages.account = messages.accountsModel[head.sections.selectedIndex]
4253- }
4254- }
4255-
4256 Loader {
4257 id: searchListLoader
4258
4259@@ -748,7 +876,7 @@
4260 topMargin: units.gu(2)
4261 left: parent.left
4262 right: parent.right
4263- bottom: bottomPanel.top
4264+ bottom: composeBar.top
4265 }
4266 z: 1
4267 Behavior on height {
4268@@ -805,160 +933,6 @@
4269 addressableFields: messages.account ? messages.account.addressableVCardFields : ["tel"] // just to have a fallback there
4270 }
4271
4272- onAccountsModelChanged: {
4273- reloadFilters = !reloadFilters
4274- }
4275-
4276- Action {
4277- id: backButton
4278- objectName: "backButton"
4279- iconName: "back"
4280- onTriggered: {
4281- if (typeof mainPage !== 'undefined') {
4282- mainPage.temporaryProperties = null
4283- }
4284- mainStack.pop()
4285- }
4286- }
4287-
4288- states: [
4289- PageHeadState {
4290- name: "selection"
4291- head: messages.head
4292- when: selectionMode
4293-
4294- backAction: Action {
4295- objectName: "selectionModeCancelAction"
4296- iconName: "back"
4297- onTriggered: messageList.cancelSelection()
4298- }
4299-
4300- actions: [
4301- Action {
4302- objectName: "selectionModeSelectAllAction"
4303- iconName: "select"
4304- onTriggered: {
4305- if (messageList.selectedItems.count === messageList.count) {
4306- messageList.clearSelection()
4307- } else {
4308- messageList.selectAll()
4309- }
4310- }
4311- },
4312- Action {
4313- objectName: "selectionModeDeleteAction"
4314- enabled: messageList.selectedItems.count > 0
4315- iconName: "delete"
4316- onTriggered: messageList.endSelection()
4317- }
4318- ]
4319- },
4320- PageHeadState {
4321- name: "groupChat"
4322- head: messages.head
4323- when: groupChat
4324- contents: header
4325- backAction: backButton
4326-
4327- actions: [
4328- Action {
4329- objectName: "groupChatAction"
4330- iconName: "contact-group"
4331- onTriggered: PopupUtils.open(participantsPopover, screenTop)
4332- }
4333- ]
4334- },
4335- PageHeadState {
4336- name: "unknownContact"
4337- head: messages.head
4338- when: participants.length == 1 && contactWatcher.isUnknown
4339- backAction: backButton
4340- contents: header
4341-
4342- actions: [
4343- Action {
4344- objectName: "contactCallAction"
4345- visible: participants.length == 1 && contactWatcher.interactive
4346- iconName: "call-start"
4347- text: i18n.tr("Call")
4348- onTriggered: {
4349- Qt.inputMethod.hide()
4350- // FIXME: support other things than just phone numbers
4351- Qt.openUrlExternally("tel:///" + encodeURIComponent(contactWatcher.identifier))
4352- }
4353- },
4354- Action {
4355- objectName: "addContactAction"
4356- visible: contactWatcher.isUnknown && participants.length == 1 && contactWatcher.interactive
4357- iconName: "contact-new"
4358- text: i18n.tr("Add")
4359- onTriggered: {
4360- Qt.inputMethod.hide()
4361- // FIXME: support other things than just phone numbers
4362- mainView.addPhoneToContact("", contactWatcher.identifier, null, null)
4363- }
4364- }
4365- ]
4366- },
4367- PageHeadState {
4368- name: "newMessage"
4369- head: messages.head
4370- when: participants.length === 0 && isReady
4371- backAction: backButton
4372- actions: [
4373- Action {
4374- objectName: "contactList"
4375- iconName: "contact"
4376- onTriggered: {
4377- Qt.inputMethod.hide()
4378- mainStack.push(Qt.resolvedUrl("NewRecipientPage.qml"), {"multiRecipient": multiRecipient, "parentPage": messages})
4379- }
4380- }
4381-
4382- ]
4383-
4384- contents: MultiRecipientInput {
4385- id: multiRecipient
4386- objectName: "multiRecipient"
4387- enabled: visible
4388- anchors {
4389- left: parent ? parent.left : undefined
4390- right: parent ? parent.right : undefined
4391- rightMargin: units.gu(2)
4392- }
4393- }
4394- },
4395- PageHeadState {
4396- name: "knownContact"
4397- head: messages.head
4398- when: participants.length == 1 && !contactWatcher.isUnknown
4399- backAction: backButton
4400- contents: header
4401- actions: [
4402- Action {
4403- objectName: "contactCallKnownAction"
4404- visible: participants.length == 1 && messages.phoneAccount
4405- iconName: "call-start"
4406- text: i18n.tr("Call")
4407- onTriggered: {
4408- Qt.inputMethod.hide()
4409- // FIXME: support other things than just phone numbers
4410- Qt.openUrlExternally("tel:///" + encodeURIComponent(contactWatcher.identifier))
4411- }
4412- },
4413- Action {
4414- objectName: "contactProfileAction"
4415- visible: !contactWatcher.isUnknown && participants.length == 1 && messages.phoneAccount
4416- iconSource: "image://theme/contact"
4417- text: i18n.tr("Contact")
4418- onTriggered: {
4419- mainView.showContactDetails(contactWatcher.contactId, null, null)
4420- }
4421- }
4422- ]
4423- }
4424- ]
4425-
4426 HistoryEventModel {
4427 id: eventModel
4428 type: HistoryThreadModel.EventTypeText
4429@@ -1023,392 +997,125 @@
4430 objectName: "messageList"
4431 visible: !isSearching
4432
4433+ Rectangle {
4434+ color: Theme.palette.normal.background
4435+ anchors.fill: parent
4436+ Image {
4437+ width: units.gu(20)
4438+ fillMode: Image.PreserveAspectFit
4439+ anchors.centerIn: parent
4440+ visible: source !== ""
4441+ source: {
4442+ var accountId = ""
4443+
4444+ if (messages.account) {
4445+ accountId = messages.account.accountId
4446+ }
4447+
4448+ if (presenceRequest.type != PresenceRequest.PresenceTypeUnknown
4449+ && presenceRequest.type != PresenceRequest.PresenceTypeUnset) {
4450+ accountId = presenceRequest.accountId
4451+ }
4452+
4453+ return telepathyHelper.accountForId(accountId).protocolInfo.backgroundImage
4454+ }
4455+ z: 1
4456+ }
4457+ z: -1
4458+ }
4459+
4460 // because of the header
4461 clip: true
4462 anchors {
4463 top: screenTop.bottom
4464 left: parent.left
4465 right: parent.right
4466- bottom: bottomPanel.top
4467+ bottom: composeBar.top
4468 }
4469 }
4470
4471- Item {
4472- id: bottomPanel
4473- property int defaultHeight: textEntry.height + units.gu(2)
4474- anchors.bottom: isSearching ? parent.bottom : keyboard.top
4475- anchors.left: parent.left
4476- anchors.right: parent.right
4477- height: {
4478- if (selectionMode || (participants.length > 0 && !contactWatcher.interactive)) {
4479- return 0
4480- } else {
4481- if (messages.height - keyboard.height - screenTop.y > defaultHeight) {
4482- return defaultHeight
4483- } else {
4484- return messages.height - keyboard.height - screenTop.y
4485- }
4486- }
4487- }
4488- visible: !selectionMode && !isSearching
4489- clip: true
4490- MouseArea {
4491- anchors.fill: parent
4492- onClicked: {
4493- messageTextArea.forceActiveFocus()
4494- }
4495- }
4496-
4497- Behavior on height {
4498- UbuntuNumberAnimation { }
4499- }
4500-
4501- ListItem.ThinDivider {
4502- anchors.top: parent.top
4503- }
4504-
4505- Icon {
4506- id: attachButton
4507- objectName: "attachButton"
4508- anchors.left: parent.left
4509- anchors.leftMargin: units.gu(2)
4510- anchors.verticalCenter: sendButton.verticalCenter
4511- height: units.gu(3)
4512- width: units.gu(3)
4513- color: "gray"
4514- name: "camera-app-symbolic"
4515- MouseArea {
4516- anchors.fill: parent
4517- anchors.margins: units.gu(-2)
4518- onClicked: {
4519- Qt.inputMethod.hide()
4520- pictureImporter.requestNewPicture()
4521- }
4522- }
4523- }
4524-
4525- StyledItem {
4526- id: textEntry
4527- property alias text: messageTextArea.text
4528- property alias inputMethodComposing: messageTextArea.inputMethodComposing
4529- property int fullSize: attachmentThumbnails.height + messageTextArea.height
4530- style: Theme.createStyleComponent("TextAreaStyle.qml", textEntry)
4531- anchors.bottomMargin: units.gu(1)
4532- anchors.bottom: parent.bottom
4533- anchors.left: attachButton.right
4534- anchors.leftMargin: units.gu(1)
4535- anchors.right: sendButton.left
4536- anchors.rightMargin: units.gu(1)
4537- height: attachments.count !== 0 ? fullSize + units.gu(1.5) : fullSize
4538- onActiveFocusChanged: {
4539- if(activeFocus) {
4540- messageTextArea.forceActiveFocus()
4541- } else {
4542- focus = false
4543- }
4544- }
4545- focus: false
4546- MouseArea {
4547- anchors.fill: parent
4548- onClicked: messageTextArea.forceActiveFocus()
4549- }
4550- Flow {
4551- id: attachmentThumbnails
4552- spacing: units.gu(1)
4553- anchors{
4554- left: parent.left
4555- right: parent.right
4556- top: parent.top
4557- topMargin: units.gu(1)
4558- leftMargin: units.gu(1)
4559- rightMargin: units.gu(1)
4560- }
4561- height: childrenRect.height
4562-
4563- Component {
4564- id: thumbnailImage
4565- UbuntuShape {
4566- property int index
4567- property string filePath
4568-
4569- width: childrenRect.width
4570- height: childrenRect.height
4571-
4572- image: Image {
4573- id: avatarImage
4574- width: units.gu(8)
4575- height: units.gu(8)
4576- sourceSize.height: height
4577- sourceSize.width: width
4578- fillMode: Image.PreserveAspectCrop
4579- source: filePath
4580- asynchronous: true
4581- }
4582- MouseArea {
4583- anchors.fill: parent
4584- onPressAndHold: {
4585- mouse.accept = true
4586- Qt.inputMethod.hide()
4587- activeAttachmentIndex = index
4588- PopupUtils.open(attachmentPopover, parent)
4589- }
4590- }
4591- }
4592- }
4593-
4594- Component {
4595- id: thumbnailContact
4596- Item {
4597- id: attachment
4598-
4599- readonly property int contactsCount:vcardParser.contacts ? vcardParser.contacts.length : 0
4600- property int index
4601- property string filePath
4602- property alias vcard: vcardParser
4603- property string contactDisplayName: {
4604- if (contactsCount > 0) {
4605- var contact = vcard.contacts[0]
4606- if (contact.displayLabel.label && (contact.displayLabel.label != "")) {
4607- return contact.displayLabel.label
4608- } else if (contact.name) {
4609- var contacFullName = contact.name.firstName
4610- if (contact.name.midleName) {
4611- contacFullName += " " + contact.name.midleName
4612- }
4613- if (contact.name.lastName) {
4614- contacFullName += " " + contact.name.lastName
4615- }
4616- return contacFullName
4617- }
4618- return i18n.tr("Unknown contact")
4619- }
4620- return ""
4621- }
4622- property string title: {
4623- var result = attachment.contactDisplayName
4624- if (attachment.contactsCount > 1) {
4625- return result + " (+%1)".arg(attachment.contactsCount-1)
4626- } else {
4627- return result
4628- }
4629- }
4630-
4631- height: units.gu(6)
4632- width: textEntry.width
4633-
4634- ContactAvatar {
4635- id: avatar
4636-
4637- anchors {
4638- top: parent.top
4639- bottom: parent.bottom
4640- left: parent.left
4641- }
4642- contactElement: attachment.contactsCount === 1 ? attachment.vcard.contacts[0] : null
4643- fallbackAvatarUrl: attachment.contactsCount === 1 ? "image://theme/contact" : "image://theme/contact-group"
4644- fallbackDisplayName: attachment.contactsCount === 1 ? attachment.contactDisplayName : ""
4645- width: height
4646- }
4647- Label {
4648- id: label
4649-
4650- anchors {
4651- left: avatar.right
4652- leftMargin: units.gu(1)
4653- top: avatar.top
4654- bottom: avatar.bottom
4655- right: parent.right
4656- rightMargin: units.gu(1)
4657- }
4658-
4659- verticalAlignment: Text.AlignVCenter
4660- text: attachment.title
4661- elide: Text.ElideMiddle
4662- color: UbuntuColors.lightAubergine
4663- }
4664- MouseArea {
4665- anchors.fill: parent
4666- onPressAndHold: {
4667- mouse.accept = true
4668- Qt.inputMethod.hide()
4669- activeAttachmentIndex = index
4670- PopupUtils.open(attachmentPopover, parent)
4671- }
4672- }
4673- VCardParser {
4674- id: vcardParser
4675-
4676- vCardUrl: attachment ? Qt.resolvedUrl(attachment.filePath) : ""
4677- }
4678- }
4679- }
4680-
4681- Component {
4682- id: thumbnailUnknown
4683-
4684- UbuntuShape {
4685- property int index
4686- property string filePath
4687-
4688- width: units.gu(8)
4689- height: units.gu(8)
4690-
4691- Icon {
4692- anchors.centerIn: parent
4693- width: units.gu(6)
4694- height: units.gu(6)
4695- name: "attachment"
4696- }
4697- MouseArea {
4698- anchors.fill: parent
4699- onPressAndHold: {
4700- mouse.accept = true
4701- Qt.inputMethod.hide()
4702- activeAttachmentIndex = index
4703- PopupUtils.open(attachmentPopover, parent)
4704- }
4705- }
4706- }
4707- }
4708-
4709- Repeater {
4710- model: attachments
4711- delegate: Loader {
4712- height: units.gu(8)
4713- sourceComponent: {
4714- var contentType = getContentType(filePath)
4715- console.log(contentType)
4716- switch(contentType) {
4717- case ContentType.Contacts:
4718- return thumbnailContact
4719- case ContentType.Pictures:
4720- return thumbnailImage
4721- case ContentType.Unknown:
4722- return thumbnailUnknown
4723- default:
4724- console.log("unknown content Type")
4725- }
4726- }
4727- onStatusChanged: {
4728- if (status == Loader.Ready) {
4729- item.index = index
4730- item.filePath = filePath
4731- }
4732- }
4733- }
4734- }
4735- }
4736-
4737- ListItem.ThinDivider {
4738- id: divider
4739-
4740- anchors {
4741- left: parent.left
4742- right: parent.right
4743- top: attachmentThumbnails.bottom
4744- margins: units.gu(0.5)
4745- }
4746- visible: attachments.count > 0
4747- }
4748-
4749- TextArea {
4750- id: messageTextArea
4751- objectName: "messageTextArea"
4752- anchors {
4753- top: attachments.count == 0 ? textEntry.top : attachmentThumbnails.bottom
4754- left: parent.left
4755- right: parent.right
4756- }
4757- // this value is to avoid letter being cut off
4758- height: units.gu(4.3)
4759- style: LocalTextAreaStyle {}
4760- autoSize: true
4761- maximumLineCount: attachments.count == 0 ? 8 : 4
4762- placeholderText: i18n.tr("Write a message...")
4763- focus: textEntry.focus
4764- font.family: "Ubuntu"
4765- font.pixelSize: FontUtils.sizeToPixels("medium")
4766- color: "#5d5d5d"
4767- text: messages.text
4768- }
4769-
4770- /*InverseMouseArea {
4771- anchors.fill: parent
4772- visible: textEntry.activeFocus
4773- onClicked: {
4774- textEntry.focus = false;
4775- }
4776- }*/
4777- Component.onCompleted: {
4778- // if page is active, it means this is not a bottom edge page
4779- if (messages.active && messages.keyboardFocus && participants.length != 0) {
4780- messageTextArea.forceActiveFocus()
4781- }
4782- }
4783- }
4784-
4785- Icon {
4786- id: sendButton
4787- objectName: "sendButton"
4788- anchors.verticalCenter: textEntry.verticalCenter
4789- anchors.right: parent.right
4790- anchors.rightMargin: units.gu(2)
4791- color: "gray"
4792- source: Qt.resolvedUrl("./assets/send.svg")
4793- width: units.gu(3)
4794- height: units.gu(3)
4795- enabled: {
4796- if (participants.length > 0 || multiRecipient.recipientCount > 0 || multiRecipient.searchString !== "") {
4797- if (textEntry.text != "" || textEntry.inputMethodComposing || attachments.count > 0) {
4798- return true
4799- }
4800- }
4801- return false
4802- }
4803-
4804- MouseArea {
4805- anchors.fill: parent
4806- anchors.margins: units.gu(-2)
4807- onClicked: {
4808- // make sure we flush everything we have prepared in the OSK preedit
4809- Qt.inputMethod.commit();
4810- if (textEntry.text == "" && attachments.count == 0) {
4811- return
4812- }
4813- // refresh the recipient list
4814- multiRecipient.focus = false
4815-
4816- if (messages.account && messages.accountId == "") {
4817- messages.accountId = messages.account.accountId
4818- messages.head.sections.selectedIndex = Qt.binding(getSelectedIndex)
4819- }
4820-
4821- var newAttachments = []
4822- for (var i = 0; i < attachments.count; i++) {
4823- var attachment = []
4824- var item = attachments.get(i)
4825- // we dont include smil files. they will be auto generated
4826- if (item.contentType.toLowerCase() === "application/smil") {
4827- continue
4828- }
4829- attachment.push(item.name)
4830- attachment.push(item.contentType)
4831- attachment.push(item.filePath)
4832- newAttachments.push(attachment)
4833- }
4834-
4835- var recipients = participantIds.length > 0 ? participantIds :
4836- multiRecipient.recipients
4837- // if sendMessage succeeds it means the message was either sent or
4838- // injected into the history service so the user can retry later
4839- if (sendMessage(textEntry.text, recipients, newAttachments)) {
4840- textEntry.text = ""
4841- attachments.clear()
4842- }
4843- if (eventModel.filter == null) {
4844- reloadFilters = !reloadFilters
4845- }
4846- }
4847+ ComposeBar {
4848+ id: composeBar
4849+ anchors {
4850+ bottom: isSearching ? parent.bottom : keyboard.top
4851+ left: parent.left
4852+ right: parent.right
4853+ }
4854+
4855+ showContents: !selectionMode && !isSearching
4856+ maxHeight: messages.height - keyboard.height - screenTop.y
4857+ text: messages.text
4858+ canSend: participants.length > 0 || multiRecipient.recipientCount > 0 || multiRecipient.searchString !== ""
4859+ oskEnabled: messages.oskEnabled
4860+
4861+ Component.onCompleted: {
4862+ // if page is active, it means this is not a bottom edge page
4863+ if (messages.active && messages.keyboardFocus && participants.length != 0) {
4864+ forceFocus()
4865+ }
4866+ }
4867+
4868+ onSendRequested: {
4869+ // refresh the recipient list
4870+ multiRecipient.focus = false
4871+
4872+ if (messages.account && messages.accountId == "") {
4873+ messages.accountId = messages.account.accountId
4874+ messages.head.sections.selectedIndex = Qt.binding(getSelectedIndex)
4875+ }
4876+
4877+ var newAttachments = []
4878+ var videoSize = 0;
4879+ for (var i = 0; i < attachments.count; i++) {
4880+ var attachment = []
4881+ var item = attachments.get(i)
4882+ // we dont include smil files. they will be auto generated
4883+ if (item.contentType.toLowerCase() === "application/smil") {
4884+ continue
4885+ }
4886+ if (startsWith(item.contentType.toLowerCase(),"video/")) {
4887+ videoSize += FileOperations.size(item.filePath)
4888+ }
4889+ attachment.push(item.name)
4890+ attachment.push(item.contentType)
4891+ attachment.push(item.filePath)
4892+ newAttachments.push(attachment)
4893+ }
4894+ if (videoSize > 307200 && !settings.messagesDontShowFileSizeWarning) {
4895+ // FIXME we are guessing here if the handler will try to send it over multimedia account
4896+ var isPhone = (account && account.type == AccountEntry.PhoneAccount)
4897+ if (isPhone) {
4898+ for (var i in telepathyHelper.accounts) {
4899+ var tmpAccount = telepathyHelper.accounts[i]
4900+ if (tmpAccount.type == AccountEntry.MultimediaAccount) {
4901+ // now check if the user is at least known by the account
4902+ if (presenceRequest.type != PresenceRequest.PresenceTypeUnknown
4903+ && presenceRequest.type != PresenceRequest.PresenceTypeUnset) {
4904+ isPhone = false
4905+ }
4906+ }
4907+ }
4908+ }
4909+
4910+ if (isPhone) {
4911+ PopupUtils.open(Qt.createComponent("Dialogs/FileSizeWarningDialog.qml").createObject(messages))
4912+ }
4913+ }
4914+
4915+ var recipients = participantIds.length > 0 ? participantIds :
4916+ multiRecipient.recipients
4917+ var properties = {}
4918+ if (composeBar.audioAttached) {
4919+ properties["x-canonical-tmp-files"] = true
4920+ }
4921+
4922+ // if sendMessage succeeds it means the message was either sent or
4923+ // injected into the history service so the user can retry later
4924+ if (sendMessage(text, recipients, newAttachments, properties)) {
4925+ composeBar.reset()
4926+ }
4927+ if (eventModel.filter == null) {
4928+ reloadFilters = !reloadFilters
4929 }
4930 }
4931 }
4932
4933=== modified file 'src/qml/MessagesHeader.qml'
4934--- src/qml/MessagesHeader.qml 2015-08-04 01:06:06 +0000
4935+++ src/qml/MessagesHeader.qml 2016-01-14 12:41:18 +0000
4936@@ -18,7 +18,7 @@
4937
4938
4939 import QtQuick 2.2
4940-import Ubuntu.Components 1.1
4941+import Ubuntu.Components 1.3
4942
4943 Item {
4944 id: header
4945@@ -26,7 +26,12 @@
4946 property string title: ""
4947 property string subtitle: ""
4948
4949- height: units.gu(7)
4950+ height: units.gu(8)
4951+
4952+ anchors {
4953+ top: parent.top
4954+ topMargin: units.gu(1)
4955+ }
4956
4957 Behavior on height {
4958 UbuntuNumberAnimation {}
4959@@ -50,7 +55,7 @@
4960 }
4961 verticalAlignment: Text.AlignVCenter
4962
4963- font.pixelSize: FontUtils.sizeToPixels("x-large")
4964+ font.pixelSize: FontUtils.sizeToPixels("large")
4965 elide: Text.ElideRight
4966 text: title
4967 }
4968
4969=== modified file 'src/qml/MessagingContactEditorPage.qml'
4970--- src/qml/MessagingContactEditorPage.qml 2016-01-06 18:29:47 +0000
4971+++ src/qml/MessagingContactEditorPage.qml 2016-01-14 12:41:18 +0000
4972@@ -28,6 +28,7 @@
4973
4974 property var contactListPage: null
4975
4976+<<<<<<< TREE
4977 leadingActions: Action {
4978 objectName: "cancel"
4979
4980@@ -36,9 +37,41 @@
4981 onTriggered: {
4982 root.cancel()
4983 root.active = false
4984+=======
4985+ header: PageHeader {
4986+ id: pageHeader
4987+
4988+ leadingActionBar {
4989+ actions: [
4990+ Action {
4991+ objectName: "cancel"
4992+
4993+ text: i18n.tr("Cancel")
4994+ iconName: "back"
4995+ onTriggered: {
4996+ root.cancel()
4997+ root.active = false
4998+ }
4999+ }
5000+ ]
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: