mirror of
https://github.com/zedeus/nitter.git
synced 2026-04-10 07:42:13 -04:00
@@ -113,7 +113,7 @@ const
|
||||
$2
|
||||
"includeHasBirdwatchNotes": false,
|
||||
"includePromotedContent": false,
|
||||
"withBirdwatchNotes": false,
|
||||
"withBirdwatchNotes": true,
|
||||
"withVoice": false,
|
||||
"withV2Timeline": true
|
||||
}""".replace(" ", "").replace("\n", "")
|
||||
|
||||
@@ -6,6 +6,12 @@ import experimental/parser/unifiedcard
|
||||
|
||||
proc parseGraphTweet(js: JsonNode): Tweet
|
||||
|
||||
proc parseCommunityNote(js: JsonNode): string =
|
||||
let subtitle = js{"subtitle"}
|
||||
result = subtitle{"text"}.getStr
|
||||
with entities, subtitle{"entities"}:
|
||||
result = expandBirdwatchEntities(result, entities)
|
||||
|
||||
proc parseUser(js: JsonNode; id=""): User =
|
||||
if js.isNull: return
|
||||
result = User(
|
||||
@@ -439,6 +445,9 @@ proc parseGraphTweet(js: JsonNode): Tweet =
|
||||
for id in ids:
|
||||
result.history.add parseBiggestInt(id.getStr)
|
||||
|
||||
with birdwatch, js{"birdwatch_pivot"}:
|
||||
result.note = parseCommunityNote(birdwatch)
|
||||
|
||||
proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] =
|
||||
for t in ? js{"content", "items"}:
|
||||
let entryId = t.getEntryId
|
||||
|
||||
@@ -330,6 +330,26 @@ proc expandNoteTweetEntities*(tweet: Tweet; js: JsonNode) =
|
||||
|
||||
tweet.text = tweet.text.multiReplace((unicodeOpen, xmlOpen), (unicodeClose, xmlClose))
|
||||
|
||||
proc expandBirdwatchEntities*(text: string; entities: JsonNode): string =
|
||||
let runes = text.toRunes
|
||||
var replacements: seq[ReplaceSlice]
|
||||
|
||||
for entity in entities:
|
||||
let
|
||||
fromIdx = entity{"from_index"}.getInt
|
||||
toIdx = entity{"to_index"}.getInt
|
||||
url = entity{"ref", "url"}.getStr
|
||||
if url.len > 0:
|
||||
replacements.add ReplaceSlice(
|
||||
kind: rkUrl,
|
||||
slice: fromIdx ..< toIdx,
|
||||
url: url,
|
||||
display: $runes[fromIdx ..< min(toIdx, runes.len)]
|
||||
)
|
||||
|
||||
replacements.sort(cmp)
|
||||
result = runes.replacedWith(replacements, 0 ..< runes.len)
|
||||
|
||||
proc extractGalleryPhoto*(t: Tweet): GalleryPhoto =
|
||||
let url =
|
||||
if t.photos.len > 0: t.photos[0].url
|
||||
|
||||
@@ -78,6 +78,9 @@ genPrefs:
|
||||
hideReplies(checkbox, false):
|
||||
"Hide tweet replies"
|
||||
|
||||
hideCommunityNotes(checkbox, false):
|
||||
"Hide community notes"
|
||||
|
||||
squareAvatars(checkbox, false):
|
||||
"Square profile pictures"
|
||||
|
||||
|
||||
@@ -254,3 +254,38 @@
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.community-note {
|
||||
background-color: var(--bg_elements);
|
||||
margin-top: 10px;
|
||||
border: solid 1px var(--dark_grey);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
pointer-events: all;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg_panel);
|
||||
border-color: var(--grey);
|
||||
}
|
||||
}
|
||||
|
||||
.community-note-header {
|
||||
background-color: var(--bg_hover);
|
||||
font-weight: 700;
|
||||
padding: 8px 10px;
|
||||
padding-top: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
|
||||
.icon-container {
|
||||
flex-shrink: 0;
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.community-note-text {
|
||||
white-space: pre-line;
|
||||
padding: 10px 10px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
@@ -1,119 +1,119 @@
|
||||
@import '_variables';
|
||||
@import '_mixins';
|
||||
@import "_variables";
|
||||
@import "_mixins";
|
||||
|
||||
.card {
|
||||
margin: 5px 0;
|
||||
pointer-events: all;
|
||||
max-height: unset;
|
||||
margin: 5px 0;
|
||||
pointer-events: all;
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
border-radius: 10px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--dark_grey);
|
||||
background-color: var(--bg_elements);
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-decoration: none !important;
|
||||
border: solid 1px var(--dark_grey);
|
||||
border-radius: 10px;
|
||||
background-color: var(--bg_elements);
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
text-decoration: none !important;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--grey);
|
||||
}
|
||||
&:hover {
|
||||
border-color: var(--grey);
|
||||
}
|
||||
|
||||
.attachments {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
.attachments {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@include ellipsis;
|
||||
white-space: unset;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
@include ellipsis;
|
||||
white-space: unset;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
margin: 0.3em 0;
|
||||
white-space: pre-wrap;
|
||||
margin: 0.3em 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.card-destination {
|
||||
@include ellipsis;
|
||||
color: var(--grey);
|
||||
display: block;
|
||||
@include ellipsis;
|
||||
color: var(--grey);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-content-container {
|
||||
color: unset;
|
||||
overflow: auto;
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
color: unset;
|
||||
overflow: auto;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-image-container {
|
||||
width: 98px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
width: 98px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
padding-top: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.card-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: var(--bg_overlays);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background-color: var(--bg_overlays);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 400px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 400px;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.card-overlay {
|
||||
@include play-button;
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@include play-button;
|
||||
opacity: 0.8;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.large {
|
||||
.card-container {
|
||||
display: block;
|
||||
}
|
||||
.card-container {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-image-container {
|
||||
width: unset;
|
||||
.card-image-container {
|
||||
width: unset;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.card-image {
|
||||
position: unset;
|
||||
border-style: solid;
|
||||
border-color: var(--dark_grey);
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
.card-image {
|
||||
position: unset;
|
||||
border-style: solid;
|
||||
border-color: var(--dark_grey);
|
||||
border-width: 0;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,31 +19,49 @@
|
||||
}
|
||||
|
||||
.tweet-name-row {
|
||||
padding: 6px 8px;
|
||||
margin-top: 1px;
|
||||
padding: 8px 10px 6px 10px;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
padding: 0px 8px 8px 8px;
|
||||
padding: 10px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.show-thread {
|
||||
padding: 0px 8px 6px 8px;
|
||||
padding: 0px 10px 6px 10px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.quote-latest {
|
||||
padding: 0px 8px 6px 8px;
|
||||
padding: 0px 10px 6px 10px;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.replying-to {
|
||||
padding: 0px 8px;
|
||||
padding: 0px 10px;
|
||||
padding-bottom: 4px;
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
.community-note {
|
||||
background-color: var(--bg_panel);
|
||||
border: unset;
|
||||
border-top: solid 1px var(--dark_grey);
|
||||
border-radius: unset;
|
||||
margin-top: 0;
|
||||
|
||||
&:hover {
|
||||
border-top-color: var(--grey);
|
||||
}
|
||||
|
||||
.community-note-header {
|
||||
background-color: var(--bg_panel);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.unavailable-quote {
|
||||
|
||||
@@ -223,6 +223,7 @@ type
|
||||
video*: Option[Video]
|
||||
photos*: seq[Photo]
|
||||
history*: seq[int64]
|
||||
note*: string
|
||||
|
||||
Tweets* = seq[Tweet]
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
||||
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
||||
|
||||
buildHtml(head):
|
||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=27")
|
||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=28")
|
||||
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
|
||||
|
||||
if theme.len > 0:
|
||||
|
||||
@@ -226,6 +226,14 @@ proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||
elif quote.gif.isSome:
|
||||
renderGif(quote.gif.get(), prefs)
|
||||
|
||||
proc renderCommunityNote(note: string; prefs: Prefs): VNode =
|
||||
buildHtml(tdiv(class="community-note")):
|
||||
tdiv(class="community-note-header"):
|
||||
icon "group"
|
||||
span: text "Community note"
|
||||
tdiv(class="community-note-text", dir="auto"):
|
||||
verbatim replaceUrls(note, prefs)
|
||||
|
||||
proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||
if not quote.available:
|
||||
return buildHtml(tdiv(class="quote unavailable")):
|
||||
@@ -261,6 +269,9 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||
if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
|
||||
renderQuoteMedia(quote, prefs, path)
|
||||
|
||||
if quote.note.len > 0 and not prefs.hideCommunityNotes:
|
||||
renderCommunityNote(quote.note, prefs)
|
||||
|
||||
if quote.hasThread:
|
||||
a(class="show-thread", href=getLink(quote)):
|
||||
text "Show this thread"
|
||||
@@ -346,6 +357,9 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
|
||||
if tweet.quote.isSome:
|
||||
renderQuote(tweet.quote.get(), prefs, path)
|
||||
|
||||
if tweet.note.len > 0 and not prefs.hideCommunityNotes:
|
||||
renderCommunityNote(tweet.note, prefs)
|
||||
|
||||
let
|
||||
hasEdits = tweet.history.len > 1
|
||||
isLatest = hasEdits and tweet.id == max(tweet.history)
|
||||
|
||||
Reference in New Issue
Block a user