mirror of
https://github.com/zedeus/nitter.git
synced 2026-04-15 18:22:11 -04:00
@@ -138,6 +138,13 @@ proc getTweet*(id: string; after=""): Future[Conversation] {.async.} =
|
|||||||
if after.len > 0:
|
if after.len > 0:
|
||||||
result.replies = await getReplies(id, after)
|
result.replies = await getReplies(id, after)
|
||||||
|
|
||||||
|
proc getGraphEditHistory*(id: string): Future[EditHistory] {.async.} =
|
||||||
|
if id.len == 0: return
|
||||||
|
let
|
||||||
|
url = apiReq(graphTweetEditHistory, tweetEditHistoryVars % id)
|
||||||
|
js = await fetch(url)
|
||||||
|
result = parseGraphEditHistory(js, id)
|
||||||
|
|
||||||
proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} =
|
proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} =
|
||||||
let q = genQueryParam(query)
|
let q = genQueryParam(query)
|
||||||
if q.len == 0 or q == emptyQuery:
|
if q.len == 0 or q == emptyQuery:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const
|
|||||||
graphTweet* = "Y4Erk_-0hObvLpz0Iw3bzA/ConversationTimeline"
|
graphTweet* = "Y4Erk_-0hObvLpz0Iw3bzA/ConversationTimeline"
|
||||||
graphTweetDetail* = "YVyS4SfwYW7Uw5qwy0mQCA/TweetDetail"
|
graphTweetDetail* = "YVyS4SfwYW7Uw5qwy0mQCA/TweetDetail"
|
||||||
graphTweetResult* = "nzme9KiYhfIOrrLrPP_XeQ/TweetResultByIdQuery"
|
graphTweetResult* = "nzme9KiYhfIOrrLrPP_XeQ/TweetResultByIdQuery"
|
||||||
|
graphTweetEditHistory* = "upS9teTSG45aljmP9oTuXA/TweetEditHistory"
|
||||||
graphSearchTimeline* = "bshMIjqDk8LTXTq4w91WKw/SearchTimeline"
|
graphSearchTimeline* = "bshMIjqDk8LTXTq4w91WKw/SearchTimeline"
|
||||||
graphListById* = "cIUpT1UjuGgl_oWiY7Snhg/ListByRestId"
|
graphListById* = "cIUpT1UjuGgl_oWiY7Snhg/ListByRestId"
|
||||||
graphListBySlug* = "K6wihoTiTrzNzSF8y1aeKQ/ListBySlug"
|
graphListBySlug* = "K6wihoTiTrzNzSF8y1aeKQ/ListBySlug"
|
||||||
@@ -29,35 +30,67 @@ const
|
|||||||
"android_ad_formats_media_component_render_overlay_enabled": false,
|
"android_ad_formats_media_component_render_overlay_enabled": false,
|
||||||
"android_graphql_skip_api_media_color_palette": false,
|
"android_graphql_skip_api_media_color_palette": false,
|
||||||
"android_professional_link_spotlight_display_enabled": false,
|
"android_professional_link_spotlight_display_enabled": false,
|
||||||
|
"articles_api_enabled": false,
|
||||||
|
"articles_preview_enabled": true,
|
||||||
"blue_business_profile_image_shape_enabled": false,
|
"blue_business_profile_image_shape_enabled": false,
|
||||||
|
"c9s_tweet_anatomy_moderator_badge_enabled": true,
|
||||||
"commerce_android_shop_module_enabled": false,
|
"commerce_android_shop_module_enabled": false,
|
||||||
|
"communities_web_enable_tweet_community_results_fetch": true,
|
||||||
|
"creator_subscriptions_quote_tweet_preview_enabled": false,
|
||||||
"creator_subscriptions_subscription_count_enabled": false,
|
"creator_subscriptions_subscription_count_enabled": false,
|
||||||
"creator_subscriptions_tweet_preview_api_enabled": true,
|
"creator_subscriptions_tweet_preview_api_enabled": true,
|
||||||
"freedom_of_speech_not_reach_fetch_enabled": true,
|
"freedom_of_speech_not_reach_fetch_enabled": true,
|
||||||
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
|
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
|
||||||
|
"grok_android_analyze_trend_fetch_enabled": false,
|
||||||
|
"grok_translations_community_note_auto_translation_is_enabled": false,
|
||||||
|
"grok_translations_community_note_translation_is_enabled": false,
|
||||||
|
"grok_translations_post_auto_translation_is_enabled": false,
|
||||||
|
"grok_translations_timeline_user_bio_auto_translation_is_enabled": false,
|
||||||
"hidden_profile_likes_enabled": false,
|
"hidden_profile_likes_enabled": false,
|
||||||
"highlights_tweets_tab_ui_enabled": false,
|
"highlights_tweets_tab_ui_enabled": false,
|
||||||
|
"immersive_video_status_linkable_timestamps": false,
|
||||||
"interactive_text_enabled": false,
|
"interactive_text_enabled": false,
|
||||||
"longform_notetweets_consumption_enabled": true,
|
"longform_notetweets_consumption_enabled": true,
|
||||||
"longform_notetweets_inline_media_enabled": true,
|
"longform_notetweets_inline_media_enabled": true,
|
||||||
"longform_notetweets_rich_text_read_enabled": true,
|
|
||||||
"longform_notetweets_richtext_consumption_enabled": true,
|
"longform_notetweets_richtext_consumption_enabled": true,
|
||||||
|
"longform_notetweets_rich_text_read_enabled": true,
|
||||||
"mobile_app_spotlight_module_enabled": false,
|
"mobile_app_spotlight_module_enabled": false,
|
||||||
|
"payments_enabled": false,
|
||||||
|
"post_ctas_fetch_enabled": true,
|
||||||
|
"premium_content_api_read_enabled": false,
|
||||||
|
"profile_label_improvements_pcf_label_in_post_enabled": true,
|
||||||
|
"profile_label_improvements_pcf_label_in_profile_enabled": false,
|
||||||
"responsive_web_edit_tweet_api_enabled": true,
|
"responsive_web_edit_tweet_api_enabled": true,
|
||||||
"responsive_web_enhance_cards_enabled": false,
|
"responsive_web_enhance_cards_enabled": false,
|
||||||
"responsive_web_graphql_exclude_directive_enabled": true,
|
"responsive_web_graphql_exclude_directive_enabled": true,
|
||||||
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
|
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
|
||||||
"responsive_web_graphql_timeline_navigation_enabled": true,
|
"responsive_web_graphql_timeline_navigation_enabled": true,
|
||||||
|
"responsive_web_grok_analysis_button_from_backend": true,
|
||||||
|
"responsive_web_grok_analyze_button_fetch_trends_enabled": false,
|
||||||
|
"responsive_web_grok_analyze_post_followups_enabled": true,
|
||||||
|
"responsive_web_grok_annotations_enabled": true,
|
||||||
|
"responsive_web_grok_community_note_auto_translation_is_enabled": false,
|
||||||
|
"responsive_web_grok_image_annotation_enabled": true,
|
||||||
|
"responsive_web_grok_imagine_annotation_enabled": true,
|
||||||
|
"responsive_web_grok_share_attachment_enabled": true,
|
||||||
|
"responsive_web_grok_show_grok_translated_post": false,
|
||||||
|
"responsive_web_jetfuel_frame": true,
|
||||||
"responsive_web_media_download_video_enabled": false,
|
"responsive_web_media_download_video_enabled": false,
|
||||||
|
"responsive_web_profile_redirect_enabled": false,
|
||||||
"responsive_web_text_conversations_enabled": false,
|
"responsive_web_text_conversations_enabled": false,
|
||||||
|
"responsive_web_twitter_article_notes_tab_enabled": false,
|
||||||
"responsive_web_twitter_article_tweet_consumption_enabled": true,
|
"responsive_web_twitter_article_tweet_consumption_enabled": true,
|
||||||
"unified_cards_destination_url_params_enabled": false,
|
|
||||||
"responsive_web_twitter_blue_verified_badge_is_enabled": true,
|
"responsive_web_twitter_blue_verified_badge_is_enabled": true,
|
||||||
"rweb_lists_timeline_redesign_enabled": true,
|
"rweb_lists_timeline_redesign_enabled": true,
|
||||||
|
"rweb_tipjar_consumption_enabled": true,
|
||||||
|
"rweb_video_screen_enabled": false,
|
||||||
|
"rweb_video_timestamps_enabled": false,
|
||||||
"spaces_2022_h2_clipping": true,
|
"spaces_2022_h2_clipping": true,
|
||||||
"spaces_2022_h2_spaces_communities": true,
|
"spaces_2022_h2_spaces_communities": true,
|
||||||
"standardized_nudges_misinfo": true,
|
"standardized_nudges_misinfo": true,
|
||||||
|
"subscriptions_feature_can_gift_premium": false,
|
||||||
"subscriptions_verification_info_enabled": true,
|
"subscriptions_verification_info_enabled": true,
|
||||||
|
"subscriptions_verification_info_is_identity_verified_enabled": false,
|
||||||
"subscriptions_verification_info_reason_enabled": true,
|
"subscriptions_verification_info_reason_enabled": true,
|
||||||
"subscriptions_verification_info_verified_since_enabled": true,
|
"subscriptions_verification_info_verified_since_enabled": true,
|
||||||
"super_follow_badge_privacy_enabled": false,
|
"super_follow_badge_privacy_enabled": false,
|
||||||
@@ -68,40 +101,10 @@ const
|
|||||||
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
|
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
|
||||||
"tweetypie_unmention_optimization_enabled": false,
|
"tweetypie_unmention_optimization_enabled": false,
|
||||||
"unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
|
"unified_cards_ad_metadata_container_dynamic_card_content_query_enabled": false,
|
||||||
|
"unified_cards_destination_url_params_enabled": false,
|
||||||
"verified_phone_label_enabled": false,
|
"verified_phone_label_enabled": false,
|
||||||
"vibe_api_enabled": false,
|
"vibe_api_enabled": false,
|
||||||
"view_counts_everywhere_api_enabled": true,
|
"view_counts_everywhere_api_enabled": true,
|
||||||
"premium_content_api_read_enabled": false,
|
|
||||||
"communities_web_enable_tweet_community_results_fetch": true,
|
|
||||||
"responsive_web_jetfuel_frame": true,
|
|
||||||
"responsive_web_grok_analyze_button_fetch_trends_enabled": false,
|
|
||||||
"responsive_web_grok_image_annotation_enabled": true,
|
|
||||||
"responsive_web_grok_imagine_annotation_enabled": true,
|
|
||||||
"rweb_tipjar_consumption_enabled": true,
|
|
||||||
"profile_label_improvements_pcf_label_in_post_enabled": true,
|
|
||||||
"creator_subscriptions_quote_tweet_preview_enabled": false,
|
|
||||||
"c9s_tweet_anatomy_moderator_badge_enabled": true,
|
|
||||||
"responsive_web_grok_analyze_post_followups_enabled": true,
|
|
||||||
"rweb_video_timestamps_enabled": false,
|
|
||||||
"responsive_web_grok_share_attachment_enabled": true,
|
|
||||||
"articles_preview_enabled": true,
|
|
||||||
"immersive_video_status_linkable_timestamps": false,
|
|
||||||
"articles_api_enabled": false,
|
|
||||||
"responsive_web_grok_analysis_button_from_backend": true,
|
|
||||||
"rweb_video_screen_enabled": false,
|
|
||||||
"payments_enabled": false,
|
|
||||||
"responsive_web_profile_redirect_enabled": false,
|
|
||||||
"responsive_web_grok_show_grok_translated_post": false,
|
|
||||||
"responsive_web_grok_community_note_auto_translation_is_enabled": false,
|
|
||||||
"profile_label_improvements_pcf_label_in_profile_enabled": false,
|
|
||||||
"grok_android_analyze_trend_fetch_enabled": false,
|
|
||||||
"grok_translations_community_note_auto_translation_is_enabled": false,
|
|
||||||
"grok_translations_post_auto_translation_is_enabled": false,
|
|
||||||
"grok_translations_community_note_translation_is_enabled": false,
|
|
||||||
"grok_translations_timeline_user_bio_auto_translation_is_enabled": false,
|
|
||||||
"subscriptions_feature_can_gift_premium": false,
|
|
||||||
"responsive_web_twitter_article_notes_tab_enabled": false,
|
|
||||||
"subscriptions_verification_info_is_identity_verified_enabled": false,
|
|
||||||
"hidden_profile_subscriptions_enabled": false
|
"hidden_profile_subscriptions_enabled": false
|
||||||
}""".replace(" ", "").replace("\n", "")
|
}""".replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
@@ -128,6 +131,11 @@ const
|
|||||||
"withVoice": true
|
"withVoice": true
|
||||||
}""".replace(" ", "").replace("\n", "")
|
}""".replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
|
tweetEditHistoryVars* = """{
|
||||||
|
"tweetId": "$1",
|
||||||
|
"withQuickPromoteEligibilityTweetFields": true
|
||||||
|
}""".replace(" ", "").replace("\n", "")
|
||||||
|
|
||||||
restIdVars* = """{
|
restIdVars* = """{
|
||||||
"rest_id": "$1", $2
|
"rest_id": "$1", $2
|
||||||
"count": 20
|
"count": 20
|
||||||
|
|||||||
@@ -165,13 +165,17 @@ proc getDuration*(video: Video): string =
|
|||||||
else:
|
else:
|
||||||
return &"{min mod 60}:{sec mod 60:02}"
|
return &"{min mod 60}:{sec mod 60:02}"
|
||||||
|
|
||||||
|
proc getLink*(id: int64; username="i"; focus=true): string =
|
||||||
|
var username = username
|
||||||
|
if username.len == 0:
|
||||||
|
username = "i"
|
||||||
|
result = &"/{username}/status/{id}"
|
||||||
|
if focus: result &= "#m"
|
||||||
|
|
||||||
proc getLink*(tweet: Tweet; focus=true): string =
|
proc getLink*(tweet: Tweet; focus=true): string =
|
||||||
if tweet.id == 0: return
|
if tweet.id == 0: return
|
||||||
var username = tweet.user.username
|
var username = tweet.user.username
|
||||||
if username.len == 0:
|
return getLink(tweet.id, username, focus)
|
||||||
username = "i"
|
|
||||||
result = &"/{username}/status/{tweet.id}"
|
|
||||||
if focus: result &= "#m"
|
|
||||||
|
|
||||||
proc getTwitterLink*(path: string; params: Table[string, string]): string =
|
proc getTwitterLink*(path: string; params: Table[string, string]): string =
|
||||||
var
|
var
|
||||||
|
|||||||
@@ -435,12 +435,16 @@ proc parseGraphTweet(js: JsonNode): Tweet =
|
|||||||
else:
|
else:
|
||||||
result.quote = some Tweet(id: js{"legacy", "quoted_status_id_str"}.getId)
|
result.quote = some Tweet(id: js{"legacy", "quoted_status_id_str"}.getId)
|
||||||
|
|
||||||
|
with ids, js{"edit_control", "edit_control_initial", "edit_tweet_ids"}:
|
||||||
|
for id in ids:
|
||||||
|
result.history.add parseBiggestInt(id.getStr)
|
||||||
|
|
||||||
proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] =
|
proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] =
|
||||||
for t in ? js{"content", "items"}:
|
for t in ? js{"content", "items"}:
|
||||||
let entryId = t.getEntryId
|
let entryId = t.getEntryId
|
||||||
if "tweet-" in entryId and "promoted" notin entryId:
|
if "tweet-" in entryId and "promoted" notin entryId:
|
||||||
let tweet = t.getTweetResult("item")
|
let tweet = t.getTweetResult("item")
|
||||||
if not tweet.isNull:
|
if tweet.notNull:
|
||||||
result.thread.content.add parseGraphTweet(tweet)
|
result.thread.content.add parseGraphTweet(tweet)
|
||||||
|
|
||||||
let tweetDisplayType = select(
|
let tweetDisplayType = select(
|
||||||
@@ -516,6 +520,29 @@ proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation =
|
|||||||
)
|
)
|
||||||
result.replies.bottom = cursorValue.getStr
|
result.replies.bottom = cursorValue.getStr
|
||||||
|
|
||||||
|
proc parseGraphEditHistory*(js: JsonNode; tweetId: string): EditHistory =
|
||||||
|
let instructions = ? js{
|
||||||
|
"data", "tweet_result_by_rest_id", "result",
|
||||||
|
"edit_history_timeline", "timeline", "instructions"
|
||||||
|
}
|
||||||
|
if instructions.len == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in instructions:
|
||||||
|
if i.getTypeName == "TimelineAddEntries":
|
||||||
|
for e in i{"entries"}:
|
||||||
|
let entryId = e.getEntryId
|
||||||
|
if entryId == "latestTweet":
|
||||||
|
with item, e{"content", "items"}[0]:
|
||||||
|
let tweetResult = item.getTweetResult("item")
|
||||||
|
if tweetResult.notNull:
|
||||||
|
result.latest = parseGraphTweet(tweetResult)
|
||||||
|
elif entryId == "staleTweets":
|
||||||
|
for item in e{"content", "items"}:
|
||||||
|
let tweetResult = item.getTweetResult("item")
|
||||||
|
if tweetResult.notNull:
|
||||||
|
result.history.add parseGraphTweet(tweetResult)
|
||||||
|
|
||||||
proc extractTweetsFromEntry*(e: JsonNode): seq[Tweet] =
|
proc extractTweetsFromEntry*(e: JsonNode): seq[Tweet] =
|
||||||
with tweetResult, getTweetResult(e):
|
with tweetResult, getTweetResult(e):
|
||||||
var tweet = parseGraphTweet(tweetResult)
|
var tweet = parseGraphTweet(tweetResult)
|
||||||
|
|||||||
@@ -64,9 +64,29 @@ proc createStatusRouter*(cfg: Config) =
|
|||||||
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle,
|
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle,
|
||||||
images=images, video=video)
|
images=images, video=video)
|
||||||
|
|
||||||
|
get "/@name/status/@id/history/?":
|
||||||
|
cond '.' notin @"name"
|
||||||
|
let id = @"id"
|
||||||
|
|
||||||
|
if id.len > 19 or id.any(c => not c.isDigit):
|
||||||
|
resp Http404, showError("Invalid tweet ID", cfg)
|
||||||
|
|
||||||
|
let edits = await getGraphEditHistory(id)
|
||||||
|
if edits.latest == nil or edits.latest.id == 0:
|
||||||
|
resp Http404, showError("Tweet history not found", cfg)
|
||||||
|
|
||||||
|
let
|
||||||
|
prefs = requestPrefs()
|
||||||
|
title = "History for " & pageTitle(edits.latest)
|
||||||
|
ogTitle = "Edit History for " & pageTitle(edits.latest.user)
|
||||||
|
desc = edits.latest.text
|
||||||
|
|
||||||
|
let html = renderEditHistory(edits, prefs, getPath())
|
||||||
|
resp renderMain(html, request, cfg, prefs, title, desc, ogTitle)
|
||||||
|
|
||||||
get "/@name/@s/@id/@m/?@i?":
|
get "/@name/@s/@id/@m/?@i?":
|
||||||
cond @"s" in ["status", "statuses"]
|
cond @"s" in ["status", "statuses"]
|
||||||
cond @"m" in ["video", "photo", "history"]
|
cond @"m" in ["video", "photo"]
|
||||||
redirect("/$1/status/$2" % [@"name", @"id"])
|
redirect("/$1/status/$2" % [@"name", @"id"])
|
||||||
|
|
||||||
get "/@name/statuses/@id/?":
|
get "/@name/statuses/@id/?":
|
||||||
|
|||||||
@@ -4,12 +4,8 @@
|
|||||||
@include panel(100%, 600px);
|
@include panel(100%, 600px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline {
|
.timeline > div:not(:first-child) {
|
||||||
background-color: var(--bg_panel);
|
|
||||||
|
|
||||||
> div:not(:first-child) {
|
|
||||||
border-top: 1px solid var(--border_grey);
|
border-top: 1px solid var(--border_grey);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-header {
|
.timeline-header {
|
||||||
@@ -159,4 +155,5 @@
|
|||||||
padding: 0.75em;
|
padding: 0.75em;
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: var(--bg_panel);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,8 +80,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tweet-published {
|
.tweet-published {
|
||||||
margin: 0;
|
margin-top: 10px;
|
||||||
margin-top: 5px;
|
margin-bottom: 3px;
|
||||||
color: var(--grey);
|
color: var(--grey);
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
@@ -242,3 +242,15 @@
|
|||||||
background-color: var(--bg_hover);
|
background-color: var(--bg_hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.latest-post-version {
|
||||||
|
border-bottom: 1px solid var(--dark_grey);
|
||||||
|
border-top: 1px solid var(--dark_grey);
|
||||||
|
padding: 01ch 0px;
|
||||||
|
margin: 1ch 0px;
|
||||||
|
color: var(--grey);
|
||||||
|
|
||||||
|
a {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,11 @@
|
|||||||
margin-top: -6px;
|
margin-top: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quote-latest {
|
||||||
|
padding: 0px 8px 6px 8px;
|
||||||
|
color: var(--grey);
|
||||||
|
}
|
||||||
|
|
||||||
.replying-to {
|
.replying-to {
|
||||||
padding: 0px 8px;
|
padding: 0px 8px;
|
||||||
margin: unset;
|
margin: unset;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
@import "_variables";
|
@import "_variables";
|
||||||
@import "_mixins";
|
@import "_mixins";
|
||||||
|
|
||||||
.conversation {
|
.conversation,
|
||||||
|
.edit-history {
|
||||||
@include panel(100%, 600px);
|
@include panel(100%, 600px);
|
||||||
|
|
||||||
.show-more {
|
.show-more {
|
||||||
@@ -9,19 +10,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-thread {
|
.main-thread,
|
||||||
|
.latest-edit {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
background-color: var(--bg_panel);
|
}
|
||||||
|
|
||||||
|
.reply {
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-tweet,
|
.main-tweet,
|
||||||
.replies {
|
.replies,
|
||||||
|
.edit-history > div {
|
||||||
body.fixed-nav & {
|
body.fixed-nav & {
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
margin-top: -50px;
|
margin-top: -50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-history-header {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: var(--bg_panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tweet-edit {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.main-tweet .tweet-content {
|
.main-tweet .tweet-content {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
@@ -32,11 +50,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply {
|
|
||||||
background-color: var(--bg_panel);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thread-line {
|
.thread-line {
|
||||||
.timeline-item::before,
|
.timeline-item::before,
|
||||||
&.timeline-item::before {
|
&.timeline-item::before {
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ type
|
|||||||
gif*: Option[Gif]
|
gif*: Option[Gif]
|
||||||
video*: Option[Video]
|
video*: Option[Video]
|
||||||
photos*: seq[Photo]
|
photos*: seq[Photo]
|
||||||
|
history*: seq[int64]
|
||||||
|
|
||||||
Tweets* = seq[Tweet]
|
Tweets* = seq[Tweet]
|
||||||
|
|
||||||
@@ -242,6 +243,10 @@ type
|
|||||||
after*: Chain
|
after*: Chain
|
||||||
replies*: Result[Chain]
|
replies*: Result[Chain]
|
||||||
|
|
||||||
|
EditHistory* = object
|
||||||
|
latest*: Tweet
|
||||||
|
history*: Tweets
|
||||||
|
|
||||||
Timeline* = Result[Tweets]
|
Timeline* = Result[Tweets]
|
||||||
|
|
||||||
Profile* = object
|
Profile* = object
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
|
|||||||
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
|
||||||
|
|
||||||
buildHtml(head):
|
buildHtml(head):
|
||||||
link(rel="stylesheet", type="text/css", href="/css/style.css?v=26")
|
link(rel="stylesheet", type="text/css", href="/css/style.css?v=27")
|
||||||
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
|
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
|
||||||
|
|
||||||
if theme.len > 0:
|
if theme.len > 0:
|
||||||
|
|||||||
@@ -78,3 +78,17 @@ proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode
|
|||||||
renderReplies(conv.replies, prefs, path, conv.tweet)
|
renderReplies(conv.replies, prefs, path, conv.tweet)
|
||||||
|
|
||||||
renderToTop(focus="#m")
|
renderToTop(focus="#m")
|
||||||
|
|
||||||
|
proc renderEditHistory*(edits: EditHistory; prefs: Prefs; path: string): VNode =
|
||||||
|
buildHtml(tdiv(class="edit-history")):
|
||||||
|
tdiv(class="latest-edit"):
|
||||||
|
tdiv(class="edit-history-header"):
|
||||||
|
text "Latest post"
|
||||||
|
renderTweet(edits.latest, prefs, path)
|
||||||
|
|
||||||
|
tdiv(class="previous-edits"):
|
||||||
|
tdiv(class="edit-history-header"):
|
||||||
|
text "Version history"
|
||||||
|
for tweet in edits.history:
|
||||||
|
tdiv(class="tweet-edit"):
|
||||||
|
renderTweet(tweet, prefs, path)
|
||||||
|
|||||||
@@ -211,6 +211,12 @@ proc renderMediaTags(tags: seq[User]): VNode =
|
|||||||
if i < tags.high:
|
if i < tags.high:
|
||||||
text ", "
|
text ", "
|
||||||
|
|
||||||
|
proc renderLatestPost(username: string; id: int64): VNode =
|
||||||
|
buildHtml(tdiv(class="latest-post-version")):
|
||||||
|
text "There's a new version of this post. "
|
||||||
|
a(href=getLink(id, username)):
|
||||||
|
text "See the latest post"
|
||||||
|
|
||||||
proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
|
proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode =
|
||||||
buildHtml(tdiv(class="quote-media-container")):
|
buildHtml(tdiv(class="quote-media-container")):
|
||||||
if quote.photos.len > 0:
|
if quote.photos.len > 0:
|
||||||
@@ -252,12 +258,16 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode =
|
|||||||
tdiv(class="quote-text", dir="auto"):
|
tdiv(class="quote-text", dir="auto"):
|
||||||
verbatim replaceUrls(quote.text, prefs)
|
verbatim replaceUrls(quote.text, prefs)
|
||||||
|
|
||||||
|
if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
|
||||||
|
renderQuoteMedia(quote, prefs, path)
|
||||||
|
|
||||||
if quote.hasThread:
|
if quote.hasThread:
|
||||||
a(class="show-thread", href=getLink(quote)):
|
a(class="show-thread", href=getLink(quote)):
|
||||||
text "Show this thread"
|
text "Show this thread"
|
||||||
|
|
||||||
if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome:
|
if quote.history.len > 0 and quote.id != max(quote.history):
|
||||||
renderQuoteMedia(quote, prefs, path)
|
tdiv(class="quote-latest"):
|
||||||
|
text "There's a new version of this post"
|
||||||
|
|
||||||
proc renderLocation*(tweet: Tweet): string =
|
proc renderLocation*(tweet: Tweet): string =
|
||||||
let (place, url) = tweet.getLocation()
|
let (place, url) = tweet.getLocation()
|
||||||
@@ -336,8 +346,20 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0;
|
|||||||
if tweet.quote.isSome:
|
if tweet.quote.isSome:
|
||||||
renderQuote(tweet.quote.get(), prefs, path)
|
renderQuote(tweet.quote.get(), prefs, path)
|
||||||
|
|
||||||
|
let
|
||||||
|
hasEdits = tweet.history.len > 1
|
||||||
|
isLatest = hasEdits and tweet.id == max(tweet.history)
|
||||||
|
|
||||||
if mainTweet:
|
if mainTweet:
|
||||||
p(class="tweet-published"): text &"{getTime(tweet)}"
|
p(class="tweet-published"):
|
||||||
|
if hasEdits and isLatest:
|
||||||
|
a(href=(getLink(tweet, focus=false) & "/history")):
|
||||||
|
text &"Last edited {getTime(tweet)}"
|
||||||
|
else:
|
||||||
|
text &"{getTime(tweet)}"
|
||||||
|
|
||||||
|
if hasEdits and not isLatest:
|
||||||
|
renderLatestPost(tweet.user.username, max(tweet.history))
|
||||||
|
|
||||||
if tweet.mediaTags.len > 0:
|
if tweet.mediaTags.len > 0:
|
||||||
renderMediaTags(tweet.mediaTags)
|
renderMediaTags(tweet.mediaTags)
|
||||||
|
|||||||
Reference in New Issue
Block a user