2
This file is part of Choqok, the KDE micro-blogging client
4
Copyright (C) 2008-2009 Mehrdad Momeny <mehrdad.momeny@gmail.com>
6
This program is free software; you can redistribute it and/or
7
modify it under the terms of the GNU General Public License as
8
published by the Free Software Foundation; either version 2 of
9
the License or (at your option) version 3 or any later version
10
accepted by the membership of KDE e.V. (or its successor approved
11
by the membership of KDE e.V.), which shall act as a proxy
12
defined in Section 14 of version 3 of the license.
15
This program is distributed in the hope that it will be useful,
16
but WITHOUT ANY WARRANTY; without even the implied warranty of
17
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
GNU General Public License for more details.
20
You should have received a copy of the GNU General Public License
21
along with this program; if not, see http://www.gnu.org/licenses/
24
#include "statuswidget.h"
26
#include "mediamanager.h"
28
#include <KNotification>
31
#include "mainwindow.h"
35
#include <KDE/KLocale>
36
#include <KToolInvocation>
37
#include "identicasearch.h"
38
#include "twittersearch.h"
40
#include <KTemporaryFile>
41
#include "userinfowidget.h"
43
static const int _15SECS = 15000;
44
static const int _MINUTE = 60000;
45
static const int _HOUR = 60*_MINUTE;
47
const QString StatusWidget::baseText("<table width=\"100%\"><tr><td rowspan=\"2\"\
48
width=\"48\">%1</td><td><p>%2</p></td></tr><tr><td style=\"font-size:small;\" align=\"right\">%3</td></tr></table>");
49
const QString StatusWidget::baseStyle("QFrame.StatusWidget {border: 1px solid rgb(150,150,150);\
51
QFrame.StatusWidget[read=false] {color: %1; background-color: %2}\
52
QFrame.StatusWidget[read=true] {color: %3; background-color: %4}");
54
QString StatusWidget::style;
56
const QRegExp StatusWidget::mUrlRegExp("((ftps?|https?)://[^\\s<>\"]+[^!,\\.\\s<>'\"\\)\\]])"); // "borrowed" from microblog plasmoid
57
const QRegExp StatusWidget::mUserRegExp("([\\s]|^)@([^\\s\\W]+)");
58
const QRegExp StatusWidget::mHashtagRegExp("([\\s]|^)#([^\\s\\W]+)");
59
const QRegExp StatusWidget::mGroupRegExp("([\\s]|^)!([^\\s\\W]+)");
61
void StatusWidget::setStyle(const QColor& color, const QColor& back, const QColor& read, const QColor& readBack)
63
style = baseStyle.arg(getColorString(color),getColorString(back),getColorString(read),getColorString(readBack));
66
StatusWidget::StatusWidget( const Account *account, QWidget *parent )
67
: KTextBrowser( parent ),mIsRead(true),mCurrentAccount(account),isBaseStatusShowed(false),
68
isMissingStatusRequested(false)
70
setSizePolicy(QSizePolicy::Preferred,QSizePolicy::Fixed);
71
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
72
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
77
timer.start( _MINUTE );
78
connect( &timer, SIGNAL( timeout() ), this, SLOT( updateSign() ) );
79
connect(this,SIGNAL(anchorClicked(QUrl)),this,SLOT(checkAnchor(QUrl)));
82
void StatusWidget::checkAnchor(const QUrl & url)
84
QString scheme = url.scheme();
85
Account::Service s = mCurrentAccount->serviceType();
87
if( scheme == "group" && ( s == Account::Identica || s == Account::Laconica ) ) {
88
type = IdenticaSearch::ReferenceGroup;
89
} else if(scheme == "tag") {
91
case Account::Identica:
92
case Account::Laconica:
93
type = IdenticaSearch::ReferenceHashtag;
95
case Account::Twitter:
96
type = TwitterSearch::ReferenceHashtag;
99
} else if(scheme == "user") {
101
KAction * info = new KAction( KIcon("user-identity"), i18n("Who is %1", url.host()), &menu );
102
KAction * from = new KAction(KIcon("edit-find-user"), i18n("From %1",url.host()),&menu);
103
KAction * to = new KAction(KIcon("meeting-attending"), i18n("Replies to %1",url.host()),&menu);
104
if(url.host().toLower() == mCurrentStatus.user.screenName.toLower())
105
menu.addAction(info);
106
menu.addAction(from);
111
case Account::Identica:
112
case Account::Laconica:
113
from->setData(IdenticaSearch::FromUser);
114
to->setData(IdenticaSearch::ToUser);
116
case Account::Twitter:
117
cont = new KAction(KIcon("user-properties"),i18n("Including %1",url.host()),&menu);
118
menu.addAction(cont);
119
from->setData(TwitterSearch::FromUser);
120
to->setData(TwitterSearch::ToUser);
121
cont->setData(TwitterSearch::ReferenceUser);
124
ret = menu.exec(QCursor::pos());
127
showUserInformation(mCurrentStatus.user);
130
type = ret->data().toInt();
131
} else if( scheme == "status" ) {
132
if(isBaseStatusShowed) {
134
isBaseStatusShowed = false;
137
Backend *b = new Backend(new Account(*mCurrentAccount), this);
138
connect( b, SIGNAL( singleStatusReceived( Status ) ),
139
this, SLOT( baseStatusReceived(Status) ) );
140
b->requestSingleStatus( url.host().toULongLong() );
143
if( Settings::useCustomBrowser() ) {
144
QStringList args = Settings::customBrowser().split(' ');
145
args.append(url.toString());
146
if( KProcess::startDetached( args ) == 0 ) {
147
KNotification *notif = new KNotification( "notify", this );
148
notif->setText( i18n("Could not launch custom browser.\nUsing KDE default browser.") );
150
KToolInvocation::invokeBrowser(url.toString());
153
KToolInvocation::invokeBrowser(url.toString());
157
emit sigSearch(type,url.host());
160
void StatusWidget::setupUi()
162
QGridLayout * buttonGrid = new QGridLayout;
164
btnReply = getButton( "btnReply",i18nc( "@info:tooltip", "Reply" ), "edit-undo" );
165
btnRemove = getButton( "btnRemove",i18nc( "@info:tooltip", "Remove" ), "edit-delete" );
166
btnFavorite = getButton( "btnFavorite",i18nc( "@info:tooltip", "Favorite" ), "rating" );
167
btnReTweet = getButton( "btnReTweet", i18nc( "@info:tooltip", "ReTweet" ), "retweet" );
168
btnFavorite->setCheckable(true);
170
buttonGrid->setRowStretch(0,100);
171
buttonGrid->setColumnStretch(5,100);
172
buttonGrid->setMargin(0);
173
buttonGrid->setSpacing(0);
175
buttonGrid->addWidget( btnReply, 1, 0 );
176
buttonGrid->addWidget( btnRemove, 1, 1 );
177
buttonGrid->addWidget( btnFavorite, 1, 2 );
178
buttonGrid->addWidget( btnReTweet, 1, 3 );
180
document()->addResource( QTextDocument::ImageResource, QUrl("icon://web"),
181
KIcon("applications-internet").pixmap(8) );
182
document()->addResource( QTextDocument::ImageResource, QUrl("img://profileImage"),
183
MediaManager::self()->defaultImage() );
184
mImage = "<img src=\"img://profileImage\" title=\""+ mCurrentStatus.user.name +"\" width=\"48\" height=\"48\" />";
186
setLayout(buttonGrid);
188
connect( btnReply, SIGNAL( clicked( bool ) ), this, SLOT( requestReply() ) );
189
connect( btnFavorite, SIGNAL( clicked( bool ) ), this, SLOT( setFavorite( bool ) ) );
190
connect( btnRemove, SIGNAL( clicked( bool ) ), this, SLOT( requestDestroy() ) );
191
connect( btnReTweet, SIGNAL( clicked( bool ) ), this, SLOT( requestReTweet() ) );
193
connect(this,SIGNAL(textChanged()),this,SLOT(setHeight()));
196
void StatusWidget::enterEvent(QEvent* event)
198
if ( !mCurrentStatus.isDMessage )
199
btnFavorite->setVisible( true );
200
if ( mCurrentStatus.user.userId != mCurrentAccount->userId() )
201
btnReply->setVisible( true );
203
btnRemove->setVisible( true );
204
btnReTweet->setVisible( true );
205
KTextBrowser::enterEvent(event);
208
void StatusWidget::leaveEvent(QEvent* event)
210
btnRemove->setVisible( false );
211
btnFavorite->setVisible( false );
212
btnReply->setVisible( false );
213
btnReTweet->setVisible( false );
215
KTextBrowser::leaveEvent(event);
218
KPushButton * StatusWidget::getButton(const QString & objName, const QString & toolTip, const QString & icon)
220
KPushButton * button = new KPushButton(KIcon(icon),QString());
221
button->setObjectName(objName);
222
button->setToolTip(toolTip);
223
button->setIconSize(QSize(8,8));
224
button->setMinimumSize(QSize(20, 20));
225
button->setMaximumSize(QSize(20, 20));
226
button->setFlat(true);
227
button->setVisible(false);
228
button->setCursor(Qt::PointingHandCursor);
232
StatusWidget::~StatusWidget()
236
void StatusWidget::setFavorite( bool isFavorite )
238
emit sigFavorite( mCurrentStatus.statusId, isFavorite );
241
Status StatusWidget::currentStatus() const
243
return mCurrentStatus;
246
void StatusWidget::setCurrentStatus( const Status newStatus )
248
mCurrentStatus = newStatus;
252
void StatusWidget::updateUi()
254
if ( mCurrentStatus.isDMessage ) {
255
btnFavorite->setVisible( false );
256
} else if ( mCurrentStatus.user.userId == mCurrentAccount->userId() ) {
257
btnReply->setVisible( false );
259
btnRemove->setVisible( false );
261
mStatus = prepareStatus(mCurrentStatus.content);
262
checkForTwitPicImages(mCurrentStatus.content);
263
mSign = generateSign();
271
void StatusWidget::setDirection()
273
QString txt = mCurrentStatus.content;
274
if(txt.startsWith('@'))
275
txt.remove(QRegExp("(^)@([^\\s\\W]+)"));
276
if(txt.startsWith('#'))
277
txt.remove(QRegExp("(^)#([^\\s\\W]+)"));
278
if(txt.startsWith('!'))
279
txt.remove(QRegExp("(^)!([^\\s\\W]+)"));
281
if( txt.isRightToLeft() ) {
282
QTextOption options(document()->defaultTextOption());
283
options.setTextDirection( Qt::RightToLeft );
284
document()->setDefaultTextOption(options);
288
void StatusWidget::setHeight()
290
document()->setTextWidth(width()-2);
291
int h = document()->size().toSize().height()+2;
296
QString StatusWidget::formatDateTime( const QDateTime &time )
298
int seconds = time.secsTo( QDateTime::currentDateTime() );
299
if ( seconds <= 15 ) {
300
timer.setInterval( _15SECS );
301
return i18n( "Just now" );
304
if ( seconds <= 45 ) {
305
timer.setInterval( _15SECS );
306
return i18np( "1 sec ago", "%1 secs ago", seconds );
309
int minutes = ( seconds - 45 + 59 ) / 60;
310
if ( minutes <= 45 ) {
311
timer.setInterval( _MINUTE );
312
return i18np( "1 min ago", "%1 mins ago", minutes );
315
int hours = ( seconds - 45 * 60 + 3599 ) / 3600;
317
timer.setInterval( _MINUTE * 15 );
318
return i18np( "1 hour ago", "%1 hours ago", hours );
321
timer.setInterval( _HOUR );
322
int days = ( seconds - 18 * 3600 + 24 * 3600 - 1 ) / ( 24 * 3600 );
323
return i18np( "1 day ago", "%1 days ago", days );
326
void StatusWidget::requestReply()
329
emit sigReply( mCurrentStatus.user.screenName, mCurrentStatus.statusId, currentStatus().isDMessage );
332
QString StatusWidget::generateSign()
335
sign = "<b><a href='user://"+mCurrentStatus.user.screenName+"' title=\"" +
336
mCurrentStatus.user.description + "\">" + mCurrentStatus.user.screenName +
337
"</a> <a href=\"" + mCurrentAccount->homepage() + mCurrentStatus.user.screenName + "\" title=\"" +
338
mCurrentStatus.user.description + "\"><img src=\"icon://web\" /></a> - </b>";
339
sign += "<a href=\"" + mCurrentAccount->statusUrl( mCurrentStatus.statusId, mCurrentStatus.user.screenName ) +
340
"\" title=\"" + mCurrentStatus.creationDateTime.toString() + "\">%1</a>";
341
if ( mCurrentStatus.isDMessage ) {
342
if( mCurrentStatus.replyToUserId == mCurrentAccount->userId() ) {
343
sign.prepend( "From " );
345
sign.prepend( "To " );
348
if( !mCurrentStatus.source.isNull() )
349
sign += " - " + mCurrentStatus.source;
350
if ( mCurrentStatus.replyToStatusId > 0 ) {
351
QString link = mCurrentAccount->statusUrl( mCurrentStatus.replyToStatusId,
352
mCurrentStatus.replyToUserScreenName );
353
sign += " - <a href='status://" + QString::number( mCurrentStatus.replyToStatusId ) + "'>" +
354
i18n("in reply to")+ "</a> <a href=\"" + link + "\"><img src=\"icon://web\" /></a>";
360
void StatusWidget::updateSign()
362
setHtml( baseText.arg( mImage, mStatus, mSign.arg( formatDateTime( mCurrentStatus.creationDateTime ) ) ) );
365
void StatusWidget::requestDestroy()
367
emit sigDestroy( mCurrentStatus.statusId );
370
void StatusWidget::requestReTweet()
372
QString text = "RT @" + mCurrentStatus.user.screenName + ' ' + mCurrentStatus.content;
373
emit sigReTweet( text );
376
void StatusWidget::checkForTwitPicImages(const QString &status)
378
///Check for twitpic images
379
if(Settings::loadTwitpicImages()) {
380
QRegExp twitPicUrlRegExp("(http://twitpic.com/[^\\s<>\"]+[^!,\\.\\s<>'\"\\]])");
381
if( status.indexOf(twitPicUrlRegExp) != -1 ) {
382
twitpicPageUrl = twitPicUrlRegExp.cap(0);
383
KUrl tempUrl( twitpicPageUrl );
384
if( tempUrl.isValid() ) {
385
kDebug()<<"Twitpic detected! "<<tempUrl.prettyUrl();
386
twitpicImageUrl = QString( "http://twitpic.com/show/mini%1" ).arg(tempUrl.path(KUrl::RemoveTrailingSlash));
387
connect( MediaManager::self(), SIGNAL( avatarFetched( const QString &, const QPixmap & ) ),
388
this, SLOT(twitpicImageFetched( const QString&, const QPixmap&)) );
389
connect( MediaManager::self(), SIGNAL(fetchError( const QString&, const QString&)),
390
this, SLOT(twitpicImageFailed( const QString&, const QString&)) );
391
MediaManager::self()->getAvatarDownloadAsyncIfNotExist( twitpicImageUrl );
397
QString StatusWidget::prepareStatus( const QString &text )
399
if( !isMissingStatusRequested && text.isEmpty() && ( mCurrentAccount->serviceType() == Account::Identica ||
400
mCurrentAccount->serviceType() == Account::Laconica ) ){
401
Backend *b = new Backend(new Account(*mCurrentAccount), this);
402
connect(b, SIGNAL(singleStatusReceived( Status )),
403
this, SLOT(missingStatusReceived( Status )));
404
b->requestSingleStatus(mCurrentStatus.statusId);
405
isMissingStatusRequested = true;
408
QString status = text;
410
status.replace( '<', "<" );
411
status.replace( '>', ">" );
412
status.replace( " www.", " http://www." );
413
if ( status.startsWith( QLatin1String("www.") ) )
414
status.prepend( "http://" );
416
status.replace(mUrlRegExp,"<a href='\\1' title='\\1'>\\1</a>");
418
if(Settings::isSmiliesEnabled())
419
status = MediaManager::self()->parseEmoticons(status);
421
status.replace(mUserRegExp,"\\1@<a href='user://\\2'>\\2</a> <a href='"+ mCurrentAccount->homepage() +
422
"\\2'><img src=\"icon://web\" /></a>");
423
if ( mCurrentAccount->serviceType() == Account::Identica ||
424
mCurrentAccount->serviceType() == Account::Laconica ) {
425
status.replace(mGroupRegExp,"\\1!<a href='group://\\2'>\\2</a> <a href='"+ mCurrentAccount->homepage() +
426
"group/\\2'><img src=\"icon://web\" /></a>");
427
status.replace(mHashtagRegExp,"\\1#<a href='tag://\\2'>\\2</a> <a href='"+ mCurrentAccount->homepage() +
428
"tag/\\1'><img src=\"icon://web\" /></a>");
430
status.replace(mHashtagRegExp,"\\1#<a href='tag://\\2'>\\2</a>");
435
QString StatusWidget::getColorString(const QColor& color)
437
return "rgb(" + QString::number(color.red()) + ',' + QString::number(color.green()) + ',' +
438
QString::number(color.blue()) + ')';
441
void StatusWidget::setUnread( Notify notifyType )
445
if ( notifyType == WithNotify ) {
446
QString name = mCurrentStatus.user.screenName;
447
QString msg = mCurrentStatus.content;
448
QPixmap icon = document()->resource(QTextDocument::ImageResource,QUrl("img://profileImage")).value<QPixmap>();
449
// QPixmap * iconUrl = MediaManager::self()->getImageLocalPathIfExist( mCurrentStatus.user.profileImageUrl );
450
if ( Settings::notifyType() == SettingsBase::KNotify ) {
451
KNotification *notify = new KNotification( "new-status-arrived", parentWidget() );
452
notify->setText( QString( "<qt><b>" + name + ":</b><br/>" + msg + "</qt>" ) );
453
if(!icon.isNull()) notify->setPixmap( icon );
454
notify->setFlags( KNotification::RaiseWidgetOnActivation | KNotification::Persistent );
455
notify->setActions( i18n( "Reply" ).split( ',' ) );
456
connect( notify, SIGNAL( action1Activated() ), this , SLOT( requestReply() ) );
458
QTimer::singleShot( Settings::notifyInterval()*1000, notify, SLOT( close() ) );
459
} else if ( Settings::notifyType() == SettingsBase::LibNotify ) {
463
tmp.setSuffix(".png");
465
icon.save(&tmp,"PNG");
466
iconArg = " -i "+tmp.fileName();
469
QString libnotifyCmd = QString( "notify-send -t " ) +
470
QString::number( Settings::notifyInterval() * 1000 ) + iconArg + QString( " -u low \"" ) +
471
name + QString( "\" \"" ) + msg + QString( "\"" );
472
KProcess::execute( libnotifyCmd );
477
void StatusWidget::setRead(bool read)
483
void StatusWidget::setUiStyle()
485
setStyleSheet( style );
488
void StatusWidget::updateFavoriteUi()
490
btnFavorite->setChecked(mCurrentStatus.isFavorited);
493
bool StatusWidget::isRead() const
498
void StatusWidget::setUserImage()
500
connect( MediaManager::self(), SIGNAL( avatarFetched( const QString &, const QPixmap & ) ),
501
this, SLOT(userAvatarFetched(const QString&, const QPixmap&)) );
502
connect( MediaManager::self(), SIGNAL(fetchError( const QString&, const QString&)),
503
this, SLOT(fetchAvatarError( const QString&, const QString&)) );
504
MediaManager::self()->getAvatarDownloadAsyncIfNotExist( mCurrentStatus.user.profileImageUrl );
507
void StatusWidget::userAvatarFetched( const QString & remotePath, const QPixmap & pixmap )
509
if ( remotePath == KUrl(mCurrentStatus.user.profileImageUrl).url(KUrl::RemoveTrailingSlash) ) {
510
QString url = "img://profileImage";
511
document()->addResource( QTextDocument::ImageResource, url, pixmap );
513
disconnect( MediaManager::self(), SIGNAL( avatarFetched( const QString &, const QPixmap & ) ),
514
this, SLOT( userAvatarFetched( const QString&, const QPixmap& ) ) );
515
disconnect( MediaManager::self(), SIGNAL(fetchError( const QString&, const QString&)),
516
this, SLOT(fetchAvatarError( const QString&, const QString&)) );
520
void StatusWidget::fetchAvatarError( const QString & avatarUrl, const QString &errMsg )
523
if( avatarUrl == KUrl(mCurrentStatus.user.profileImageUrl).url(KUrl::RemoveTrailingSlash) ){
524
///Avatar fetching is failed! but will not disconnect to get the img if it fetches later!
525
QString url = "img://profileImage";
526
document()->addResource( QTextDocument::ImageResource, url, KIcon("image-missing").pixmap(48) );
531
void StatusWidget::missingStatusReceived( Status status )
533
if( mCurrentStatus.statusId == mCurrentStatus.statusId ){
534
mCurrentStatus = status;
536
sender()->deleteLater();
540
void StatusWidget::resizeEvent(QResizeEvent* event)
543
KTextBrowser::resizeEvent(event);
546
void StatusWidget::baseStatusReceived( Status status )
548
if(isBaseStatusShowed)
550
isBaseStatusShowed = true;
552
if( Settings::isCustomUi() ) {
553
color = Settings::defaultForeColor().lighter().name();
555
color = this->palette().dark().color().name();
557
QString baseStatusText = "<p style=\"margin-top:10px; margin-bottom:10px; margin-left:20px;\
558
margin-right:20px; -qt-block-indent:0; text-indent:0px\"><span style=\" color:" + color + ";\">";
559
baseStatusText += "<b><a href='user://"+ status.user.screenName +"'>" + status.user.screenName + "</a> :</b> ";
560
baseStatusText += prepareStatus( status.content ) + "</p>";
561
mStatus.prepend( baseStatusText );
565
void StatusWidget::twitpicImageFetched( const QString &imageUrl, const QPixmap & pixmap )
567
if(imageUrl == twitpicImageUrl) {
569
disconnect( MediaManager::self(), SIGNAL( avatarFetched( const QString &, const QPixmap & ) ),
570
this, SLOT(twitpicImageFetched( const QString&, const QPixmap&)) );
571
disconnect( MediaManager::self(), SIGNAL(fetchError( const QString&, const QString&)),
572
this, SLOT(twitpicImageFailed( const QString&, const QString&)) );
573
QString url = "img://twitpicImage";
574
document()->addResource( QTextDocument::ImageResource, url, pixmap );
575
QRegExp rx( '>' + twitpicPageUrl + '<');
576
mStatus.replace(rx, "><img src=\"img://twitpicImage\" /><");
581
void StatusWidget::twitpicImageFailed( const QString &imageUrl, const QString &errMsg )
583
if(imageUrl == twitpicImageUrl) {
585
disconnect( MediaManager::self(), SIGNAL( avatarFetched( const QString &, const QPixmap & ) ),
586
this, SLOT(twitpicImageFetched( const QString&, const QPixmap&)) );
587
disconnect( MediaManager::self(), SIGNAL(fetchError( const QString&, const QString&)),
588
this, SLOT(twitpicImageFailed( const QString&, const QString&)) );
592
void StatusWidget::showUserInformation(const User& user)
594
UserInfoWidget *widget = new UserInfoWidget(user, this);
595
widget->show(QCursor::pos());
598
#include "statuswidget.moc"