File indexing completed on 2024-05-12 05:04:16
0001 // SPDX-FileCopyrightText: 2021 kaniini <https://git.pleroma.social/kaniini> 0002 // SPDX-License-Identifier: GPL-3.0-only 0003 0004 #include "post.h" 0005 0006 #include "account/abstractaccount.h" 0007 #include "utils/utils.h" 0008 0009 #include <KLocalizedString> 0010 0011 using namespace Qt::Literals::StringLiterals; 0012 0013 static QMap<QString, Attachment::AttachmentType> stringToAttachmentType = { 0014 {QStringLiteral("image"), Attachment::AttachmentType::Image}, 0015 {QStringLiteral("gifv"), Attachment::AttachmentType::GifV}, 0016 {QStringLiteral("video"), Attachment::AttachmentType::Video}, 0017 {QStringLiteral("unknown"), Attachment::AttachmentType::Unknown}, 0018 }; 0019 0020 Attachment::Attachment(QObject *parent) 0021 : QObject(parent) 0022 { 0023 } 0024 0025 Attachment::Attachment(const QJsonObject &obj, QObject *parent) 0026 : QObject(parent) 0027 { 0028 fromJson(obj); 0029 } 0030 0031 void Attachment::fromJson(const QJsonObject &obj) 0032 { 0033 if (!obj.contains("type"_L1)) { 0034 m_type = Unknown; 0035 return; 0036 } 0037 0038 m_id = obj["id"_L1].toString(); 0039 m_url = obj["url"_L1].toString(); 0040 m_preview_url = obj["preview_url"_L1].toString(); 0041 m_remote_url = obj["remote_url"_L1].toString(); 0042 0043 setDescription(obj["description"_L1].toString()); 0044 m_blurhash = obj["blurhash"_L1].toString(); 0045 m_sourceHeight = obj["meta"_L1].toObject()["original"_L1].toObject()["height"_L1].toInt(); 0046 m_sourceWidth = obj["meta"_L1].toObject()["original"_L1].toObject()["width"_L1].toInt(); 0047 0048 // determine type if we can 0049 const auto type = obj["type"_L1].toString(); 0050 if (stringToAttachmentType.contains(type)) { 0051 m_type = stringToAttachmentType[type]; 0052 } 0053 0054 if (!m_remote_url.isEmpty()) { 0055 const auto mimeType = QMimeDatabase().mimeTypeForFile(m_remote_url); 0056 if (m_type == AttachmentType::Unknown && mimeType.name().contains("image"_L1)) { 0057 m_type = AttachmentType::Image; 0058 } 0059 } 0060 0061 if (obj.contains("meta"_L1) && obj["meta"_L1].toObject().contains("focus"_L1)) { 0062 m_focusX = obj["meta"_L1].toObject()["focus"_L1].toObject()["x"_L1].toDouble(); 0063 m_focusY = obj["meta"_L1].toObject()["focus"_L1].toObject()["y"_L1].toDouble(); 0064 } 0065 } 0066 0067 QString Post::type() const 0068 { 0069 return QStringLiteral("post"); 0070 } 0071 0072 QString Attachment::description() const 0073 { 0074 return m_description; 0075 } 0076 0077 void Attachment::setDescription(const QString &description) 0078 { 0079 m_description = description; 0080 } 0081 0082 QString Attachment::id() const 0083 { 0084 return m_id; 0085 } 0086 0087 int Attachment::isVideo() const 0088 { 0089 if (m_type == AttachmentType::GifV || m_type == AttachmentType::Video) { 0090 return 1; 0091 } 0092 0093 return 0; 0094 } 0095 0096 QString Attachment::tempSource() const 0097 { 0098 return QStringLiteral("image://blurhash/%1").arg(m_blurhash); 0099 } 0100 0101 double Attachment::focusX() const 0102 { 0103 return m_focusX; 0104 } 0105 0106 void Attachment::setFocusX(double value) 0107 { 0108 if (value != m_focusX) { 0109 m_focusX = value; 0110 Q_EMIT focusXChanged(); 0111 } 0112 } 0113 0114 double Attachment::focusY() const 0115 { 0116 return m_focusY; 0117 } 0118 0119 void Attachment::setFocusY(double value) 0120 { 0121 if (value != m_focusY) { 0122 m_focusY = value; 0123 Q_EMIT focusYChanged(); 0124 } 0125 } 0126 0127 Post::Post(AbstractAccount *account, QObject *parent) 0128 : QObject(parent) 0129 , m_parent(account) 0130 , m_attachmentList(this, &m_attachments) 0131 { 0132 QString visibilityString = account->identity()->visibility(); 0133 m_visibility = stringToVisibility(visibilityString); 0134 } 0135 0136 QString computeContent(const QJsonObject &obj, std::shared_ptr<Identity> authorIdentity) 0137 { 0138 const auto emojis = CustomEmoji::parseCustomEmojis(obj["emojis"_L1].toArray()); 0139 QString content = CustomEmoji::replaceCustomEmojis(emojis, obj["content"_L1].toString()); 0140 0141 const auto tags = obj["tags"_L1].toArray(); 0142 const QString baseUrl = authorIdentity->url().toDisplayString(QUrl::RemovePath); 0143 0144 for (const auto &tag : tags) { 0145 const auto tagObj = tag.toObject(); 0146 0147 const QList<QString> tagFormats = { 0148 QStringLiteral("tags"), // Mastodon 0149 QStringLiteral("tag") // Akkoma/Pleroma 0150 }; 0151 0152 for (const QString &tagFormat : tagFormats) { 0153 content = content.replace(baseUrl + QStringLiteral("/%1/").arg(tagFormat) + tagObj["name"_L1].toString(), 0154 QStringLiteral("hashtag:/") + tagObj["name"_L1].toString(), 0155 Qt::CaseInsensitive); 0156 } 0157 } 0158 0159 const auto mentions = obj["mentions"_L1].toArray(); 0160 0161 for (const auto &mention : mentions) { 0162 const auto mentionObj = mention.toObject(); 0163 content = content.replace(mentionObj["url"_L1].toString(), QStringLiteral("account:/") + mentionObj["id"_L1].toString(), Qt::CaseInsensitive); 0164 } 0165 0166 return content; 0167 } 0168 0169 Post::Post(AbstractAccount *account, QJsonObject obj, QObject *parent) 0170 : QObject(parent) 0171 , m_parent(account) 0172 , m_attachmentList(this, &m_attachments) 0173 , m_visibility(Post::Visibility::Public) 0174 { 0175 fromJson(obj); 0176 } 0177 0178 Post *Notification::createPost(AbstractAccount *account, const QJsonObject &obj, QObject *parent) 0179 { 0180 if (!obj.empty()) { 0181 return new Post(account, obj, parent); 0182 } 0183 0184 return nullptr; 0185 } 0186 0187 void Post::fromJson(QJsonObject obj) 0188 { 0189 const auto accountDoc = obj["account"_L1].toObject(); 0190 const auto accountId = accountDoc["id"_L1].toString(); 0191 0192 m_originalPostId = obj["id"_L1].toString(); 0193 const auto reblogObj = obj["reblog"_L1].toObject(); 0194 0195 if (!obj.contains("reblog"_L1) || reblogObj.isEmpty()) { 0196 m_boosted = false; 0197 m_authorIdentity = m_parent->identityLookup(accountId, accountDoc); 0198 } else { 0199 m_boosted = true; 0200 0201 const auto reblogAccountDoc = reblogObj["account"_L1].toObject(); 0202 const auto reblogAccountId = reblogAccountDoc["id"_L1].toString(); 0203 0204 m_authorIdentity = m_parent->identityLookup(reblogAccountId, reblogAccountDoc); 0205 m_boostIdentity = m_parent->identityLookup(accountId, accountDoc); 0206 0207 obj = reblogObj; 0208 } 0209 0210 m_postId = obj["id"_L1].toString(); 0211 0212 m_spoilerText = obj["spoiler_text"_L1].toString(); 0213 0214 // First process HTML for links, and custom emojis 0215 const QString computedContent = computeContent(obj, m_authorIdentity); 0216 0217 // And then parse the content for standalone tags 0218 auto [processedContent, standaloneTags] = parseContent(computedContent); 0219 m_content = processedContent; 0220 m_standaloneTags = standaloneTags; 0221 0222 m_replyTargetId = obj["in_reply_to_id"_L1].toString(); 0223 0224 if (obj.contains("in_reply_to_account_id"_L1) && obj["in_reply_to_account_id"_L1].isString()) { 0225 if (m_parent->identityCached(obj["in_reply_to_account_id"_L1].toString())) { 0226 m_replyIdentity = m_parent->identityLookup(obj["in_reply_to_account_id"_L1].toString(), {}); 0227 } else { 0228 const auto accountId = obj["in_reply_to_account_id"_L1].toString(); 0229 QUrl uriAccount(m_parent->instanceUri()); 0230 uriAccount.setPath(QStringLiteral("/api/v1/accounts/%1").arg(accountId)); 0231 0232 m_parent->get(uriAccount, true, this, [this, accountId](QNetworkReply *reply) { 0233 const auto data = reply->readAll(); 0234 const auto doc = QJsonDocument::fromJson(data); 0235 0236 m_replyIdentity = m_parent->identityLookup(accountId, doc.object()); 0237 Q_EMIT replyIdentityChanged(); 0238 }); 0239 } 0240 } else if (!m_replyTargetId.isEmpty()) { 0241 // Fallback to getting the account id from the status, which is weird but this sometimes has to happen. 0242 m_parent->get(m_parent->apiUrl(QStringLiteral("/api/v1/statuses/%1").arg(m_replyTargetId)), true, this, [this](QNetworkReply *reply) { 0243 const auto data = reply->readAll(); 0244 const auto doc = QJsonDocument::fromJson(data); 0245 0246 m_replyIdentity = m_parent->identityLookup(doc["account"_L1].toObject()["id"_L1].toString(), doc["account"_L1].toObject()); 0247 Q_EMIT replyIdentityChanged(); 0248 }); 0249 } 0250 0251 m_url = QUrl(obj["url"_L1].toString()); 0252 0253 m_favouritesCount = obj["favourites_count"_L1].toInt(); 0254 m_reblogsCount = obj["reblogs_count"_L1].toInt(); 0255 m_repliesCount = obj["replies_count"_L1].toInt(); 0256 0257 m_favourited = obj["favourited"_L1].toBool(); 0258 m_reblogged = obj["reblogged"_L1].toBool(); 0259 m_bookmarked = obj["bookmarked"_L1].toBool(); 0260 m_pinned = obj["pinned"_L1].toBool(); 0261 m_muted = obj["muted"_L1].toBool(); 0262 0263 m_filters.clear(); 0264 0265 const auto filters = obj["filtered"_L1].toArray(); 0266 for (const auto &filter : filters) { 0267 const auto filterContext = filter.toObject(); 0268 const auto filterObj = filterContext["filter"_L1].toObject(); 0269 m_filters << filterObj["title"_L1].toString(); 0270 0271 const auto filterAction = filterObj["filter_action"_L1]; 0272 if (filterAction == "warn"_L1) { 0273 m_filtered = true; 0274 } else if (filterAction == "hide"_L1) { 0275 m_hidden = true; 0276 } 0277 } 0278 0279 m_sensitive = obj["sensitive"_L1].toBool(); 0280 m_visibility = stringToVisibility(obj["visibility"_L1].toString()); 0281 m_language = obj["language"_L1].toString(); 0282 0283 m_publishedAt = QDateTime::fromString(obj["created_at"_L1].toString(), Qt::ISODate).toLocalTime(); 0284 0285 if (!obj["edited_at"_L1].isNull()) { 0286 m_editedAt = QDateTime::fromString(obj["edited_at"_L1].toString(), Qt::ISODate).toLocalTime(); 0287 } 0288 0289 m_attachments.clear(); 0290 addAttachments(obj["media_attachments"_L1].toArray()); 0291 const QJsonArray mentions = obj["mentions"_L1].toArray(); 0292 if (obj.contains("card"_L1) && !obj["card"_L1].toObject().empty()) { 0293 setCard(std::make_optional<Card>(obj["card"_L1].toObject())); 0294 } 0295 0296 if (obj.contains("application"_L1) && !obj["application"_L1].toObject().empty()) { 0297 setApplication(std::make_optional<Application>(obj["application"_L1].toObject())); 0298 } 0299 0300 m_mentions.clear(); 0301 for (const auto &m : std::as_const(mentions)) { 0302 const QJsonObject o = m.toObject(); 0303 m_mentions.push_back(QStringLiteral("@") + o["acct"_L1].toString()); 0304 } 0305 0306 if (obj.contains(QStringLiteral("poll")) && !obj[QStringLiteral("poll")].isNull()) { 0307 m_poll = std::make_unique<Poll>(obj[QStringLiteral("poll")].toObject()); 0308 } 0309 0310 m_attachments_visible = !m_sensitive; 0311 } 0312 0313 void Post::addAttachments(const QJsonArray &attachments) 0314 { 0315 for (const auto &attachment : attachments) { 0316 m_attachments.append(new Attachment{attachment.toObject(), this}); 0317 } 0318 } 0319 0320 void Post::addAttachment(const QJsonObject &attachment) 0321 { 0322 auto att = new Attachment{attachment, this}; 0323 if (att->m_url.isEmpty()) { 0324 return; 0325 } 0326 m_attachments.append(att); 0327 0328 Q_EMIT attachmentUploaded(); 0329 } 0330 0331 void Post::setInReplyTo(const QString &inReplyTo) 0332 { 0333 if (inReplyTo == m_replyTargetId) { 0334 return; 0335 } 0336 m_replyTargetId = inReplyTo; 0337 Q_EMIT inReplyToChanged(); 0338 } 0339 0340 int Post::repliesCount() const 0341 { 0342 return m_repliesCount; 0343 } 0344 0345 QString Post::inReplyTo() const 0346 { 0347 return m_replyTargetId; 0348 } 0349 0350 void Post::setDirtyAttachment() 0351 { 0352 m_parent->invalidatePost(this); 0353 } 0354 0355 QStringList Post::mentions() const 0356 { 0357 return m_mentions; 0358 } 0359 0360 QStringList Post::filters() const 0361 { 0362 return m_filters; 0363 } 0364 0365 QUrl Post::url() const 0366 { 0367 return m_url; 0368 } 0369 0370 void Post::setMentions(const QStringList &mentions) 0371 { 0372 if (mentions == m_mentions) { 0373 return; 0374 } 0375 m_mentions = mentions; 0376 Q_EMIT mentionsChanged(); 0377 } 0378 0379 QDateTime Post::publishedAt() const 0380 { 0381 return m_publishedAt; 0382 } 0383 0384 QString Post::relativeTime() const 0385 { 0386 const auto current = QDateTime::currentDateTime(); 0387 const auto publishingDate = publishedAt(); 0388 const auto secsTo = publishingDate.secsTo(current); 0389 const auto daysTo = publishingDate.daysTo(current); 0390 if (secsTo < 0) { 0391 return i18n("in the future"); 0392 } else if (secsTo < 60) { 0393 return i18n("%1s", qCeil(secsTo)); 0394 } else if (secsTo < 60 * 60) { 0395 return i18n("%1m", qCeil(secsTo / 60)); 0396 } else if (secsTo < 60 * 60 * 24) { 0397 return i18n("%1h", qCeil(secsTo / (60 * 60))); 0398 } else if (daysTo < 7) { 0399 return i18n("%1d", qCeil(daysTo)); 0400 } else if (daysTo < 365) { 0401 const auto weeksTo = qCeil(daysTo / 7); 0402 if (weeksTo < 5) { 0403 return i18np("1 week ago", "%1 weeks ago", weeksTo); 0404 } else { 0405 const auto monthsTo = qCeil(daysTo / 30); 0406 return i18np("1 month ago", "%1 months ago", monthsTo); 0407 } 0408 } else { 0409 const auto yearsTo = qCeil(daysTo / 365); 0410 return i18np("1 year ago", "%1 years ago", yearsTo); 0411 } 0412 } 0413 0414 QString Post::absoluteTime() const 0415 { 0416 return QLocale::system().toString(publishedAt(), QLocale::LongFormat); 0417 } 0418 0419 QString Post::editedAt() const 0420 { 0421 return QLocale::system().toString(m_editedAt, QLocale::ShortFormat); 0422 } 0423 0424 bool Post::wasEdited() const 0425 { 0426 return m_editedAt.isValid(); 0427 } 0428 0429 int Post::favouritesCount() const 0430 { 0431 return m_favouritesCount; 0432 } 0433 0434 int Post::reblogsCount() const 0435 { 0436 return m_reblogsCount; 0437 } 0438 0439 static QMap<QString, Notification::Type> str_to_not_type = { 0440 {QStringLiteral("favourite"), Notification::Type::Favorite}, 0441 {QStringLiteral("follow"), Notification::Type::Follow}, 0442 {QStringLiteral("mention"), Notification::Type::Mention}, 0443 {QStringLiteral("reblog"), Notification::Type::Repeat}, 0444 {QStringLiteral("update"), Notification::Type::Update}, 0445 {QStringLiteral("poll"), Notification::Type::Poll}, 0446 {QStringLiteral("status"), Notification::Type::Status}, 0447 {QStringLiteral("follow_request"), Notification::Type::FollowRequest}, 0448 }; 0449 0450 Notification::Notification(AbstractAccount *account, const QJsonObject &obj, QObject *parent) 0451 : m_account(account) 0452 { 0453 const auto accountObj = obj["account"_L1].toObject(); 0454 const auto status = obj["status"_L1].toObject(); 0455 const auto accountId = accountObj["id"_L1].toString(); 0456 const auto type = obj["type"_L1].toString(); 0457 0458 m_post = createPost(m_account, status, parent); 0459 m_identity = m_account->identityLookup(accountId, accountObj); 0460 m_type = str_to_not_type[type]; 0461 m_id = obj["id"_L1].toString().toInt(); 0462 } 0463 0464 int Notification::id() const 0465 { 0466 return m_id; 0467 } 0468 0469 AbstractAccount *Notification::account() const 0470 { 0471 return m_account; 0472 } 0473 0474 Notification::Type Notification::type() const 0475 { 0476 return m_type; 0477 } 0478 0479 Post *Notification::post() const 0480 { 0481 return m_post; 0482 } 0483 0484 std::shared_ptr<Identity> Notification::identity() const 0485 { 0486 return m_identity; 0487 } 0488 0489 QString Post::spoilerText() const 0490 { 0491 return m_spoilerText; 0492 } 0493 0494 void Post::setSpoilerText(const QString &spoilerText) 0495 { 0496 if (spoilerText == m_spoilerText) { 0497 return; 0498 } 0499 m_spoilerText = spoilerText; 0500 Q_EMIT spoilerTextChanged(); 0501 } 0502 0503 QString Post::content() const 0504 { 0505 return m_content; 0506 } 0507 0508 void Post::setContent(const QString &content) 0509 { 0510 if (content == m_content) { 0511 return; 0512 } 0513 m_content = content; 0514 Q_EMIT contentChanged(); 0515 } 0516 0517 QVector<QString> Post::standaloneTags() const 0518 { 0519 return m_standaloneTags; 0520 } 0521 0522 QString Post::contentType() const 0523 { 0524 return m_content_type; 0525 } 0526 0527 void Post::setContentType(const QString &contentType) 0528 { 0529 if (m_content_type == contentType) { 0530 return; 0531 } 0532 m_content_type = contentType; 0533 Q_EMIT contentTypeChanged(); 0534 } 0535 0536 bool Post::sensitive() const 0537 { 0538 return m_sensitive; 0539 } 0540 0541 void Post::setSensitive(bool sensitive) 0542 { 0543 if (m_sensitive == sensitive) { 0544 return; 0545 } 0546 m_sensitive = sensitive; 0547 Q_EMIT sensitiveChanged(); 0548 } 0549 0550 Post::Visibility Post::visibility() const 0551 { 0552 return m_visibility; 0553 } 0554 0555 void Post::setVisibility(Visibility visibility) 0556 { 0557 if (visibility == m_visibility) { 0558 return; 0559 } 0560 m_visibility = visibility; 0561 Q_EMIT visibilityChanged(); 0562 } 0563 0564 QString Post::language() const 0565 { 0566 return m_language; 0567 } 0568 0569 void Post::setLanguage(const QString &language) 0570 { 0571 if (language == m_language) { 0572 return; 0573 } 0574 m_language = language; 0575 Q_EMIT languageChanged(); 0576 } 0577 0578 std::optional<Card> Post::card() const 0579 { 0580 return m_card; 0581 } 0582 0583 Card *Post::getCard() const 0584 { 0585 if (m_card.has_value()) { 0586 return const_cast<Card *>(&m_card.value()); 0587 } else { 0588 return nullptr; 0589 } 0590 } 0591 0592 void Post::setCard(std::optional<Card> card) 0593 { 0594 m_card = card; 0595 } 0596 0597 std::optional<Application> Post::application() const 0598 { 0599 return m_application; 0600 } 0601 0602 void Post::setApplication(std::optional<Application> application) 0603 { 0604 m_application = application; 0605 } 0606 0607 bool Post::favourited() const 0608 { 0609 return m_favourited; 0610 } 0611 0612 void Post::setFavourited(bool favourited) 0613 { 0614 m_favourited = favourited; 0615 } 0616 0617 bool Post::reblogged() const 0618 { 0619 return m_reblogged; 0620 } 0621 0622 void Post::setReblogged(bool reblogged) 0623 { 0624 m_reblogged = reblogged; 0625 } 0626 0627 bool Post::muted() const 0628 { 0629 return m_muted; 0630 } 0631 0632 void Post::setMuted(bool muted) 0633 { 0634 m_muted = muted; 0635 } 0636 0637 bool Post::bookmarked() const 0638 { 0639 return m_bookmarked; 0640 } 0641 0642 void Post::setBookmarked(bool bookmarked) 0643 { 0644 m_bookmarked = bookmarked; 0645 } 0646 0647 bool Post::pinned() const 0648 { 0649 return m_pinned; 0650 } 0651 0652 void Post::setPinned(bool pinned) 0653 { 0654 m_pinned = pinned; 0655 } 0656 0657 bool Post::filtered() const 0658 { 0659 return m_filtered; 0660 } 0661 0662 QList<Attachment *> Post::attachments() const 0663 { 0664 return m_attachments; 0665 } 0666 0667 QQmlListProperty<Attachment> Post::attachmentList() const 0668 { 0669 return m_attachmentList; 0670 } 0671 0672 void Post::setAttachmentsVisible(bool attachmentsVisible) 0673 { 0674 m_attachments_visible = attachmentsVisible; 0675 } 0676 0677 bool Post::attachmentsVisible() const 0678 { 0679 return m_attachments_visible; 0680 } 0681 0682 bool Post::boosted() const 0683 { 0684 return m_boosted; 0685 } 0686 0687 Card::Card(QJsonObject card) 0688 : m_card(card) 0689 { 0690 } 0691 0692 QString Card::authorName() const 0693 { 0694 return m_card[QLatin1String("author_name")].toString(); 0695 } 0696 0697 QString Card::authorUrl() const 0698 { 0699 return m_card[QLatin1String("author_url")].toString(); 0700 } 0701 0702 QString Card::blurhash() const 0703 { 0704 return m_card[QLatin1String("blurhash")].toString(); 0705 } 0706 0707 QString Card::description() const 0708 { 0709 return m_card[QLatin1String("description")].toString(); 0710 } 0711 0712 QString Card::embedUrl() const 0713 { 0714 return m_card[QLatin1String("embed_url")].toString(); 0715 } 0716 0717 int Card::width() const 0718 { 0719 return m_card[QLatin1String("weight")].toInt(); 0720 } 0721 0722 int Card::height() const 0723 { 0724 return m_card[QLatin1String("height")].toInt(); 0725 } 0726 0727 QString Card::html() const 0728 { 0729 return m_card[QLatin1String("html")].toString(); 0730 } 0731 0732 QString Card::image() const 0733 { 0734 return m_card[QLatin1String("image")].toString(); 0735 } 0736 0737 QString Card::providerName() const 0738 { 0739 const auto providerName = m_card[QLatin1String("provider_name")].toString(); 0740 if (!providerName.isEmpty()) { 0741 return providerName; 0742 } 0743 return url().host(); 0744 } 0745 0746 QString Card::providerUrl() const 0747 { 0748 return m_card[QLatin1String("provider_url")].toString(); 0749 } 0750 0751 QString Card::title() const 0752 { 0753 return m_card[QLatin1String("title")].toString().trimmed(); 0754 } 0755 0756 QUrl Card::url() const 0757 { 0758 return QUrl::fromUserInput(m_card[QLatin1String("url")].toString()); 0759 } 0760 0761 Application::Application(QJsonObject application) 0762 : m_application(application) 0763 { 0764 } 0765 0766 QString Application::name() const 0767 { 0768 return m_application[QLatin1String("name")].toString(); 0769 } 0770 0771 QUrl Application::website() const 0772 { 0773 return QUrl::fromUserInput(m_application[QLatin1String("website")].toString()); 0774 } 0775 0776 Identity *Post::getAuthorIdentity() const 0777 { 0778 return authorIdentity().get(); 0779 } 0780 0781 std::shared_ptr<Identity> Post::authorIdentity() const 0782 { 0783 return m_authorIdentity; 0784 } 0785 0786 std::shared_ptr<Identity> Post::boostIdentity() const 0787 { 0788 return m_boostIdentity; 0789 } 0790 0791 std::shared_ptr<Identity> Post::replyIdentity() const 0792 { 0793 return m_replyIdentity; 0794 } 0795 0796 Poll *Post::poll() const 0797 { 0798 return m_poll.get(); 0799 } 0800 0801 void Post::setPollJson(const QJsonObject &object) 0802 { 0803 m_poll = std::make_unique<Poll>(object); 0804 Q_EMIT pollChanged(); 0805 } 0806 0807 QString Post::postId() const 0808 { 0809 return m_postId; 0810 } 0811 0812 QString Post::originalPostId() const 0813 { 0814 return m_originalPostId; 0815 } 0816 0817 bool Post::isEmpty() const 0818 { 0819 return m_postId.isEmpty(); 0820 } 0821 0822 QPair<QString, QList<QString>> Post::parseContent(const QString &html) 0823 { 0824 const QRegularExpression hashtagExp(QStringLiteral("(?:<a\\b[^>]*>#<span>(\\S*)<\\/span><\\/a>)")); 0825 const QRegularExpression extraneousParagraph(QStringLiteral("(\\s*(?:<(?:p|br)\\s*\\/?>)+\\s*<\\/p>)")); 0826 0827 QList<QString> standaloneTags; 0828 0829 // Find the last <p> or <br> 0830 const int lastBreak = html.lastIndexOf(QStringLiteral("<br>")); 0831 int lastParagraphBegin = html.lastIndexOf(QStringLiteral("<p>")); 0832 if (lastBreak > lastParagraphBegin) { 0833 lastParagraphBegin = lastBreak; 0834 } 0835 0836 const int lastParagraphEnd = html.lastIndexOf(QStringLiteral("</p>")); 0837 QString lastParagraph = html.mid(lastParagraphBegin, lastParagraphEnd - html.length()); 0838 0839 QString processedHtml = html; 0840 0841 // Catch all the tags in the last paragraph of the post, but only if they are not surrounded by text 0842 { 0843 QList<QString> possibleTags; 0844 QString possibleLastParagraph = lastParagraph; 0845 0846 auto matchIterator = hashtagExp.globalMatch(possibleLastParagraph); 0847 while (matchIterator.hasNext()) { 0848 const QRegularExpressionMatch match = matchIterator.next(); 0849 possibleTags.push_back(match.captured(1)); 0850 possibleLastParagraph = possibleLastParagraph.replace(match.captured(0), QStringLiteral("")); 0851 } 0852 0853 // If this paragraph is truly extraneous, then we can take its tags, otherwise skip. 0854 auto extraneousIterator = extraneousParagraph.globalMatch(possibleLastParagraph); 0855 if (extraneousIterator.hasNext()) { 0856 processedHtml.replace(lastParagraph, possibleLastParagraph); 0857 standaloneTags = possibleTags; 0858 } 0859 } 0860 0861 const QRegularExpression extraneousBreakExp(QStringLiteral("(\\s*(?:<br\\s*\\/?>)+\\s*)<\\/p>")); 0862 0863 // Ensure we remove any remaining <br>'s which will mess up the spacing in a post. 0864 // Example: "<p>Yosemite Valley reflections with rock<br /> </p>" 0865 { 0866 auto matchIterator = extraneousBreakExp.globalMatch(processedHtml); 0867 while (matchIterator.hasNext()) { 0868 const QRegularExpressionMatch match = matchIterator.next(); 0869 processedHtml = processedHtml.replace(match.captured(1), QStringLiteral("")); 0870 } 0871 } 0872 0873 // Ensure we remove any empty <p>'s which will mess up the spacing in a post. 0874 // Example: "<p>Boris Karloff (again) as Imhotep</p><p> </p>" 0875 { 0876 auto matchIterator = extraneousParagraph.globalMatch(processedHtml); 0877 while (matchIterator.hasNext()) { 0878 const QRegularExpressionMatch match = matchIterator.next(); 0879 processedHtml = processedHtml.replace(match.captured(1), QStringLiteral("")); 0880 } 0881 } 0882 0883 return {processedHtml, standaloneTags}; 0884 } 0885 0886 #include "moc_post.cpp"