diff --git a/src/apps/web/TODO b/src/apps/web/TODO
index 47e6fd0..36517ef 100644
--- a/src/apps/web/TODO
+++ b/src/apps/web/TODO
@@ -1,5 +1,4 @@
read
- * nostr: links not replaced in feed titles
* support nprofile/nevent/etc links
example nevent: https://oddbean.com/e/note1qmye0at28we63aze93xjr92nzw725td0a5ncz3htwlc3wg78kp6q7802ad
example nprofile: https://oddbean.com/e/note1ykjalrpaj6jvxeuc434yd7ksrj8yd2vte478700ta8np250l3clsyjvh4q
diff --git a/src/apps/web/WebData.h b/src/apps/web/WebData.h
index 5e49a97..1efedac 100644
--- a/src/apps/web/WebData.h
+++ b/src/apps/web/WebData.h
@@ -264,11 +264,12 @@ struct Event {
// FIXME: Don't truncate UTF-8 mid-sequence
// FIXME: Don't put ellipsis if truncated text ends in punctuation
- Summary summaryHtml() const {
+ Summary summaryHtml(lmdb::txn &txn, Decompressor &decomp, UserCache &userCache) const {
Summary output;
std::string content = json.at("content").get_string();
auto firstUrl = stripUrls(content);
+ preprocessEventContent(txn, decomp, userCache, content, false);
auto textAbbrev = [](std::string &str, size_t maxLen){
if (str.size() > maxLen) str = str.substr(0, maxLen-3) + "...";
@@ -329,78 +330,83 @@ struct Event {
}
}
}
+
+ void preprocessEventContent(lmdb::txn &txn, Decompressor &decomp, UserCache &userCache, std::string &content, bool withLinks = true) const {
+ static RE2 matcher(R"((?is)(.*?)(https?://\S+|#\[\d+\]|nostr:(?:note|npub)1\w+))");
+
+ std::string output;
+
+ std::string_view contentSv(content);
+ re2::StringPiece input(contentSv);
+ re2::StringPiece prefix, match;
+
+ auto sv = [](re2::StringPiece s){ return std::string_view(s.data(), s.size()); };
+ auto appendLink = [&](std::string_view url, std::string_view text){
+ if (withLinks) {
+ output += "";
+ output += text;
+ output += "";
+ } else {
+ output += text;
+ }
+ };
+
+ while (RE2::Consume(&input, matcher, &prefix, &match)) {
+ output += sv(prefix);
+
+ if (match.starts_with("http")) {
+ appendLink(sv(match), sv(match));
+ } else if (match.starts_with("nostr:note1")) {
+ std::string path = "/e/";
+ path += sv(match).substr(6);
+ appendLink(path, sv(match));
+ } else if (match.starts_with("nostr:npub1")) {
+ bool didTransform = false;
+
+ try {
+ const auto *u = userCache.getUser(txn, decomp, decodeBech32Simple(sv(match).substr(6)));
+ appendLink(std::string("/u/") + u->npubId, std::string("@") + u->username);
+ didTransform = true;
+ } catch(std::exception &e) {
+ //LW << "tag parse error: " << e.what();
+ }
+
+ if (!didTransform) output += sv(match);
+ } else if (match.starts_with("#[")) {
+ bool didTransform = false;
+ auto offset = std::stoull(std::string(sv(match)).substr(2, match.size() - 3));
+
+ const auto &tags = json.at("tags").get_array();
+
+ try {
+ const auto &tag = tags.at(offset).get_array();
+
+ if (tag.at(0) == "p") {
+ const auto *u = userCache.getUser(txn, decomp, from_hex(tag.at(1).get_string()));
+ appendLink(std::string("/u/") + u->npubId, u->username);
+ didTransform = true;
+ } else if (tag.at(0) == "e") {
+ appendLink(std::string("/e/") + encodeBech32Simple("note", from_hex(tag.at(1).get_string())), sv(match));
+ didTransform = true;
+ }
+ } catch(std::exception &e) {
+ //LW << "tag parse error: " << e.what();
+ }
+
+ if (!didTransform) output += sv(match);
+ }
+ }
+
+ if (output.size()) {
+ output += std::string_view(input.data(), input.size());
+ std::swap(output, content);
+ }
+ }
};
-inline void preprocessEventContent(lmdb::txn &txn, Decompressor &decomp, const Event &ev, UserCache &userCache, std::string &content) {
- static RE2 matcher(R"((?is)(.*?)(https?://\S+|#\[\d+\]|nostr:(?:note|npub)1\w+))");
-
- std::string output;
-
- std::string_view contentSv(content);
- re2::StringPiece input(contentSv);
- re2::StringPiece prefix, match;
-
- auto sv = [](re2::StringPiece s){ return std::string_view(s.data(), s.size()); };
- auto appendLink = [&](std::string_view url, std::string_view text){
- output += "";
- output += text;
- output += "";
- };
-
- while (RE2::Consume(&input, matcher, &prefix, &match)) {
- output += sv(prefix);
-
- if (match.starts_with("http")) {
- appendLink(sv(match), sv(match));
- } else if (match.starts_with("nostr:note1")) {
- std::string path = "/e/";
- path += sv(match).substr(6);
- appendLink(path, sv(match));
- } else if (match.starts_with("nostr:npub1")) {
- bool didTransform = false;
-
- try {
- const auto *u = userCache.getUser(txn, decomp, decodeBech32Simple(sv(match).substr(6)));
- appendLink(std::string("/u/") + u->npubId, std::string("@") + u->username);
- didTransform = true;
- } catch(std::exception &e) {
- //LW << "tag parse error: " << e.what();
- }
-
- if (!didTransform) output += sv(match);
- } else if (match.starts_with("#[")) {
- bool didTransform = false;
- auto offset = std::stoull(std::string(sv(match)).substr(2, match.size() - 3));
-
- const auto &tags = ev.json.at("tags").get_array();
-
- try {
- const auto &tag = tags.at(offset).get_array();
-
- if (tag.at(0) == "p") {
- const auto *u = userCache.getUser(txn, decomp, from_hex(tag.at(1).get_string()));
- appendLink(std::string("/u/") + u->npubId, u->username);
- didTransform = true;
- } else if (tag.at(0) == "e") {
- appendLink(std::string("/e/") + encodeBech32Simple("note", from_hex(tag.at(1).get_string())), sv(match));
- didTransform = true;
- }
- } catch(std::exception &e) {
- //LW << "tag parse error: " << e.what();
- }
-
- if (!didTransform) output += sv(match);
- }
- }
-
- if (output.size()) {
- output += std::string_view(input.data(), input.size());
- std::swap(output, content);
- }
-}
inline std::string stripUrls(std::string &content) {
@@ -530,14 +536,14 @@ struct EventThread {
}
- std::string getThreadTitle() {
+ std::string getThreadTitle(lmdb::txn &txn, Decompressor &decomp, UserCache &userCache) {
if (!rootEventId.size()) return "";
auto p = eventCache.find(rootEventId);
if (p == eventCache.end()) return "";
const auto &elem = p->second;
- return elem.summaryHtml().text;
+ return elem.summaryHtml(txn, decomp, userCache).text;
}
@@ -564,10 +570,10 @@ struct EventThread {
ctx.abbrev = focusOnPubkey && *focusOnPubkey != pubkey;
if (ctx.abbrev) {
- ctx.content = elem.summaryHtml().text;
+ ctx.content = elem.summaryHtml(txn, decomp, userCache).text;
} else {
ctx.content = templarInternal::htmlEscape(elem.json.at("content").get_string(), false);
- preprocessEventContent(txn, decomp, elem, userCache, ctx.content);
+ elem.preprocessEventContent(txn, decomp, userCache, ctx.content);
}
ctx.ev = &elem;
diff --git a/src/apps/web/WebReader.cpp b/src/apps/web/WebReader.cpp
index 99f4c08..c6aaed3 100644
--- a/src/apps/web/WebReader.cpp
+++ b/src/apps/web/WebReader.cpp
@@ -104,7 +104,7 @@ TemplarResult renderFeed(lmdb::txn &txn, Decompressor &decomp, UserCache &userCa
auto ev = Event::fromLevId(txn, fe.levId);
ev.populateJson(txn, decomp);
- auto summary = ev.summaryHtml();
+ auto summary = ev.summaryHtml(txn, decomp, userCache);
std::string url;
if (summary.url.size()) {
url = summary.url;
@@ -208,7 +208,7 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom
if (u.path.size() == 2) {
EventThread et(txn, decomp, decodeBech32Simple(u.path[1]));
body = et.render(txn, decomp, userCache);
- title = et.getThreadTitle();
+ title = et.getThreadTitle(txn, decomp, userCache);
} else if (u.path.size() == 3) {
if (u.path[2] == "reply") {
auto ev = Event::fromIdExternal(txn, u.path[1]);