twitterchiver/viewer: support quoted tweets
This commit is contained in:
parent
7003f84fb7
commit
787e89c24d
2 changed files with 105 additions and 33 deletions
|
@ -14,12 +14,12 @@ html, body {
|
|||
display: block;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid rgb(136, 153, 166);
|
||||
border-bottom: 1px solid rgb(56, 68, 77);
|
||||
width: clamp(calc(45ch+64px), 100%, calc(75ch+64px));
|
||||
}
|
||||
.tweet-list-item:first-of-type {
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid rgb(136, 153, 166);
|
||||
border-top: 1px solid rgb(56, 68, 77);
|
||||
}
|
||||
.tweet {
|
||||
margin-left: 64px;
|
||||
|
@ -53,10 +53,10 @@ a:hover {
|
|||
.byline-link:hover .byline-name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.byline-name {
|
||||
.byline-name, .quoted-byline-name {
|
||||
color: white;
|
||||
}
|
||||
.byline, .retweeted-byline, .retweeted-icon {
|
||||
.byline, .retweeted-byline, .retweeted-icon, .quoted-byline {
|
||||
color: rgb(136, 153, 166);
|
||||
}
|
||||
.retweeted-byline {
|
||||
|
@ -95,6 +95,24 @@ a:hover {
|
|||
.media-video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quoted-tweet {
|
||||
border-radius: 15px;
|
||||
border-color: rgb(56, 68, 77);
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
margin-top: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
.quoted-byline {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.quoted-tweet-author-img {
|
||||
border-radius: 100%;
|
||||
height: 20px;
|
||||
vertical-align: bottom;
|
||||
margin-right: 2px;
|
||||
}
|
||||
</style>
|
||||
<h1>Twitterchiver: {{.TwitterUsername}}</h1>
|
||||
{{if .Query}}
|
||||
|
@ -127,9 +145,23 @@ a:hover {
|
|||
<div class="content"><!--
|
||||
-->{{call $.FormatTweetText $status.full_text $status}}<!--
|
||||
--></div>
|
||||
{{if .Object.extended_entities}}
|
||||
{{if .QuotedObject}}
|
||||
<div class="quoted-tweet">
|
||||
<div class="quoted-byline">
|
||||
<img class="quoted-tweet-author-img" src="{{.QuotedObject.user.profile_image_url_https}}">
|
||||
<strong class="quoted-byline-name">{{.QuotedObject.user.name}}</strong>
|
||||
<span class="quoted-byline-username">@{{.QuotedObject.user.screen_name}}
|
||||
·
|
||||
<time datetime="{{.QuotedCreatedAt.Format "2006-01-02T15:04:05-0700"}}">{{.QuotedCreatedAtFriendly}}</time>
|
||||
</div>
|
||||
<div class="quoted-content"><!--
|
||||
-->{{if .QuotedObject.full_text}}{{.QuotedObject.full_text}}{{else}}{{.QuotedObject.text}}{{end}}<!--
|
||||
--></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if $status.extended_entities}}
|
||||
<div class="media">
|
||||
{{range $entity := .Object.extended_entities.media}}
|
||||
{{range $entity := $status.extended_entities.media}}
|
||||
<div class="media-item">
|
||||
{{if eq $entity.type "video"}}
|
||||
<video controls poster="{{rewriteURL $entity.media_url_https}}" class="media-video">
|
||||
|
|
|
@ -256,17 +256,25 @@ SELECT
|
|||
t.id,
|
||||
t.text,
|
||||
t.object,
|
||||
CAST(COALESCE(t.object->'retweeted_status'->>'created_at', t.object->>'created_at') AS timestamp with time zone) created_at
|
||||
rt.object retweet_object,
|
||||
qt.object quote_object,
|
||||
CAST(COALESCE(t.object->'retweeted_status'->>'created_at', t.object->>'created_at') AS timestamp with time zone) created_at,
|
||||
CAST(qt.object->>'created_at' AS timestamp with time zone) quoted_created_at
|
||||
FROM
|
||||
user_accounts_tweets uat
|
||||
INNER JOIN
|
||||
user_accounts ua ON ua.userid=uat.userid
|
||||
INNER JOIN tweets t ON t.id=uat.tweetid
|
||||
INNER JOIN
|
||||
tweets t ON t.id=uat.tweetid
|
||||
LEFT JOIN
|
||||
tweets rt ON rt.id=CAST(t.object->'retweeted_status'->>'id' AS BIGINT)
|
||||
LEFT JOIN
|
||||
tweets qt ON qt.id=CAST(t.object->>'quoted_status_id' AS BIGINT)
|
||||
WHERE 1=1
|
||||
AND ua.username=$1
|
||||
AND uat.on_timeline
|
||||
AND ($3::bigint=0 OR t.id <= $3::bigint)
|
||||
AND ($4='' OR ($4<>'' AND (to_tsvector('english', text) @@ to_tsquery('english', $4) OR to_tsvector('english', object->'retweeted_status'->>'full_text') @@ to_tsquery('english', $4) OR object->'user'->>'screen_name'=$4)))
|
||||
AND ($4='' OR ($4<>'' AND (to_tsvector('english', t.text) @@ to_tsquery('english', $4) OR to_tsvector('english', t.object->'retweeted_status'->>'full_text') @@ to_tsquery('english', $4) OR t.object->'user'->>'screen_name'=$4)))
|
||||
ORDER BY t.id DESC
|
||||
LIMIT $2
|
||||
`, twitterUser, pageSize+1, startFrom, query)
|
||||
|
@ -282,6 +290,10 @@ LIMIT $2
|
|||
CreatedAt time.Time
|
||||
CreatedAtFriendly string
|
||||
Object interface{}
|
||||
RetweetedObject interface{}
|
||||
QuotedObject interface{}
|
||||
QuotedCreatedAt time.Time
|
||||
QuotedCreatedAtFriendly string
|
||||
}
|
||||
type twitterData struct {
|
||||
TwitterUsername string
|
||||
|
@ -447,29 +459,40 @@ LIMIT $2
|
|||
now := time.Now()
|
||||
for rows.Next() {
|
||||
var t tweet
|
||||
var o pgtype.JSONB
|
||||
if err := rows.Scan(&t.ID, &t.Text, &o, &t.CreatedAt); err != nil {
|
||||
var o, ro, qo pgtype.JSONB
|
||||
var qca pgtype.Timestamptz
|
||||
if err := rows.Scan(&t.ID, &t.Text, &o, &ro, &qo, &t.CreatedAt, &qca); err != nil {
|
||||
writeError(rw, http.StatusInternalServerError, "reading from database", err)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(o.Bytes, &t.Object); err != nil {
|
||||
writeError(rw, http.StatusInternalServerError, "parsing JSON from database", err)
|
||||
if o.Status == pgtype.Present {
|
||||
var oo interface{}
|
||||
if err := json.Unmarshal(o.Bytes, &oo); err != nil {
|
||||
writeError(rw, http.StatusInternalServerError, "parsing JSON from database (object)", err)
|
||||
return
|
||||
}
|
||||
ago := now.Sub(t.CreatedAt)
|
||||
switch {
|
||||
case t.CreatedAt.Year() != now.Year():
|
||||
t.CreatedAtFriendly = t.CreatedAt.Format("Jan 2, 2006")
|
||||
case t.CreatedAt.YearDay() != now.YearDay():
|
||||
t.CreatedAtFriendly = t.CreatedAt.Format("Jan 2")
|
||||
case ago.Hours() >= 1.0:
|
||||
t.CreatedAtFriendly = fmt.Sprintf("%dh", int(ago.Hours()))
|
||||
case ago.Minutes() >= 1.0:
|
||||
t.CreatedAtFriendly = fmt.Sprintf("%dm", int(ago.Minutes()))
|
||||
case ago.Seconds() >= 0.0:
|
||||
t.CreatedAtFriendly = fmt.Sprintf("%ds", int(ago.Seconds()))
|
||||
default:
|
||||
t.CreatedAtFriendly = fmt.Sprintf("in %ds", -int(ago.Seconds()))
|
||||
t.Object = oo
|
||||
t.CreatedAtFriendly = agoFriendly(now, t.CreatedAt)
|
||||
}
|
||||
if ro.Status == pgtype.Present {
|
||||
var oo interface{}
|
||||
if err := json.Unmarshal(ro.Bytes, &oo); err != nil {
|
||||
writeError(rw, http.StatusInternalServerError, "parsing JSON from database (retweeted object)", err)
|
||||
return
|
||||
}
|
||||
t.RetweetedObject = oo
|
||||
}
|
||||
if qo.Status == pgtype.Present {
|
||||
var oo interface{}
|
||||
if err := json.Unmarshal(qo.Bytes, &oo); err != nil {
|
||||
writeError(rw, http.StatusInternalServerError, "parsing JSON from database (quoted object)", err)
|
||||
return
|
||||
}
|
||||
t.QuotedObject = oo
|
||||
}
|
||||
if qca.Status == pgtype.Present {
|
||||
t.QuotedCreatedAt = qca.Time
|
||||
t.QuotedCreatedAtFriendly = agoFriendly(now, qca.Time)
|
||||
}
|
||||
td.Tweets = append(td.Tweets, t)
|
||||
}
|
||||
|
@ -488,3 +511,20 @@ LIMIT $2
|
|||
log.Printf("now listening on :8080")
|
||||
log.Print(http.ListenAndServe(":8080", r))
|
||||
}
|
||||
func agoFriendly(now, at time.Time) string {
|
||||
ago := now.Sub(at)
|
||||
switch {
|
||||
case at.Year() != now.Year():
|
||||
return at.Format("Jan 2, 2006")
|
||||
case at.YearDay() != now.YearDay():
|
||||
return at.Format("Jan 2")
|
||||
case ago.Hours() >= 1.0:
|
||||
return fmt.Sprintf("%dh", int(ago.Hours()))
|
||||
case ago.Minutes() >= 1.0:
|
||||
return fmt.Sprintf("%dm", int(ago.Minutes()))
|
||||
case ago.Seconds() >= 0.0:
|
||||
return fmt.Sprintf("%ds", int(ago.Seconds()))
|
||||
default:
|
||||
return fmt.Sprintf("in %ds", -int(ago.Seconds()))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue