Big front-end refactor to make CSS management more tractable

- Convert most CSS class names to BEM style
- Improve a significant amount of layouts / UI bugs
- Probably add a bunch of UI bugs
This commit is contained in:
Alessio 2024-04-05 15:49:19 -07:00
parent aeb2782356
commit 8410182129
28 changed files with 1557 additions and 1260 deletions

View File

@ -95,7 +95,7 @@ func TestUserFeedWithEntityInBio(t *testing.T) {
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
bio_entities := cascadia.QueryAll(root, selector(".user-bio .entity")) bio_entities := cascadia.QueryAll(root, selector(".user-header__bio .entity"))
require.Len(bio_entities, 1) require.Len(bio_entities, 1)
assert.Equal(bio_entities[0].FirstChild.Data, "@SheathUnderwear") assert.Equal(bio_entities[0].FirstChild.Data, "@SheathUnderwear")
} }
@ -192,7 +192,7 @@ func TestUserFollowers(t *testing.T) {
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-container > .user")), 2) assert.Len(cascadia.QueryAll(root, selector(".users-list > .user")), 2)
} }
func TestUserFollowees(t *testing.T) { func TestUserFollowees(t *testing.T) {
@ -204,7 +204,7 @@ func TestUserFollowees(t *testing.T) {
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-container > .user")), 1) assert.Len(cascadia.QueryAll(root, selector(".users-list > .user")), 1)
} }
// Timeline page // Timeline page
@ -363,7 +363,7 @@ func TestSearchUsers(t *testing.T) {
require.Equal(resp.StatusCode, 200) require.Equal(resp.StatusCode, 200)
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
user_elements := cascadia.QueryAll(root, selector(".users-list-container .user")) user_elements := cascadia.QueryAll(root, selector(".users-list .user"))
assert.Len(user_elements, 2) assert.Len(user_elements, 2)
assert.Contains(cascadia.Query(root, selector("#search-bar")).Attr, html.Attribute{Key: "value", Val: "no"}) assert.Contains(cascadia.Query(root, selector("#search-bar")).Attr, html.Attribute{Key: "value", Val: "no"})
} }
@ -456,7 +456,7 @@ func TestTweetsWithContent(t *testing.T) {
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".poll")), 1) assert.Len(cascadia.QueryAll(root, selector(".poll")), 1)
assert.Len(cascadia.QueryAll(root, selector(".poll-choice")), 4) assert.Len(cascadia.QueryAll(root, selector(".poll__choice")), 4)
// Video // Video
resp = do_request(httptest.NewRequest("GET", "/tweet/1453461248142495744", nil)) resp = do_request(httptest.NewRequest("GET", "/tweet/1453461248142495744", nil))
@ -478,7 +478,7 @@ func TestTweetsWithContent(t *testing.T) {
root, err = html.Parse(resp.Body) root, err = html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".space")), 1) assert.Len(cascadia.QueryAll(root, selector(".space")), 1)
assert.Len(cascadia.QueryAll(root, selector("ul.space-participants-list li")), 9) assert.Len(cascadia.QueryAll(root, selector("ul.space__participants-list li")), 9)
} }
func TestTweetWithEntities(t *testing.T) { func TestTweetWithEntities(t *testing.T) {
@ -621,7 +621,7 @@ func TestListsIndex(t *testing.T) {
require.NoError(err) require.NoError(err)
// Check that there's at least 2 Lists // Check that there's at least 2 Lists
assert.True(t, len(cascadia.QueryAll(root, selector(".users-list-preview"))) >= 2) assert.True(t, len(cascadia.QueryAll(root, selector(".list-preview"))) >= 2)
} }
func TestListDetail(t *testing.T) { func TestListDetail(t *testing.T) {
@ -633,7 +633,7 @@ func TestListDetail(t *testing.T) {
require.Equal(resp.StatusCode, 200) require.Equal(resp.StatusCode, 200)
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-container .author-info")), 5) assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 5)
// Feed // Feed
resp1 := do_request(httptest.NewRequest("GET", "/lists/2", nil)) resp1 := do_request(httptest.NewRequest("GET", "/lists/2", nil))
@ -662,7 +662,7 @@ func TestListAddAndDeleteUser(t *testing.T) {
require.Equal(resp.StatusCode, 200) require.Equal(resp.StatusCode, 200)
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-container .author-info")), 2) assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 2)
// Add a user // Add a user
resp_add := do_request(httptest.NewRequest("GET", "/lists/2/add_user?user_handle=cernovich", nil)) resp_add := do_request(httptest.NewRequest("GET", "/lists/2/add_user?user_handle=cernovich", nil))
@ -674,7 +674,7 @@ func TestListAddAndDeleteUser(t *testing.T) {
require.Equal(resp.StatusCode, 200) require.Equal(resp.StatusCode, 200)
root, err = html.Parse(resp.Body) root, err = html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-container .author-info")), 3) assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 3)
// Delete a user // Delete a user
resp_remove := do_request(httptest.NewRequest("GET", "/lists/2/remove_user?user_handle=cernovich", nil)) resp_remove := do_request(httptest.NewRequest("GET", "/lists/2/remove_user?user_handle=cernovich", nil))
@ -686,7 +686,7 @@ func TestListAddAndDeleteUser(t *testing.T) {
require.Equal(resp.StatusCode, 200) require.Equal(resp.StatusCode, 200)
root, err = html.Parse(resp.Body) root, err = html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-container .author-info")), 2) assert.Len(cascadia.QueryAll(root, selector(".users-list .author-info")), 2)
} }
func TestCreateNewList(t *testing.T) { func TestCreateNewList(t *testing.T) {
@ -698,7 +698,7 @@ func TestCreateNewList(t *testing.T) {
require.Equal(resp.StatusCode, 200) require.Equal(resp.StatusCode, 200)
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
num_lists := len(cascadia.QueryAll(root, selector(".users-list-preview"))) num_lists := len(cascadia.QueryAll(root, selector(".list-preview")))
// Create a new list // Create a new list
resp_add := do_request(httptest.NewRequest("POST", "/lists", strings.NewReader(`{"name": "My New List"}`))) resp_add := do_request(httptest.NewRequest("POST", "/lists", strings.NewReader(`{"name": "My New List"}`)))
@ -710,7 +710,7 @@ func TestCreateNewList(t *testing.T) {
require.Equal(resp.StatusCode, 200) require.Equal(resp.StatusCode, 200)
root, err = html.Parse(resp.Body) root, err = html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".users-list-preview")), num_lists+1) assert.Len(cascadia.QueryAll(root, selector(".list-preview")), num_lists+1)
} }
// Messages // Messages
@ -732,8 +732,8 @@ func TestMessagesIndexPage(t *testing.T) {
resp := recorder.Result() resp := recorder.Result()
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat")), 2) assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat-list-entry")), 2)
assert.Len(cascadia.QueryAll(root, selector(".chat-view .dm-message-and-reacts-container")), 0) // No messages until you click on one assert.Len(cascadia.QueryAll(root, selector(".chat-view .dm-message")), 0) // No messages until you click on one
} }
// Open a chat room // Open a chat room
@ -752,8 +752,8 @@ func TestMessagesRoom(t *testing.T) {
resp := recorder.Result() resp := recorder.Result()
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat")), 2) // Chat list still renders assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat-list-entry")), 2) // Chat list still renders
assert.Len(cascadia.QueryAll(root, selector("#chat-view .dm-message-and-reacts-container")), 5) assert.Len(cascadia.QueryAll(root, selector("#chat-view .dm-message")), 5)
// Should have the poller at the bottom // Should have the poller at the bottom
node := cascadia.Query(root, selector("#new-messages-poller")) node := cascadia.Query(root, selector("#new-messages-poller"))
@ -782,7 +782,7 @@ func TestMessagesRoomPollForUpdates(t *testing.T) {
resp := recorder.Result() resp := recorder.Result()
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".dm-message-and-reacts-container")), 3) assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 3)
// Should have the poller at the bottom // Should have the poller at the bottom
node := cascadia.Query(root, selector("#new-messages-poller")) node := cascadia.Query(root, selector("#new-messages-poller"))
@ -811,7 +811,7 @@ func TestMessagesRoomPollForUpdatesEmptyResult(t *testing.T) {
resp := recorder.Result() resp := recorder.Result()
root, err := html.Parse(resp.Body) root, err := html.Parse(resp.Body)
require.NoError(err) require.NoError(err)
assert.Len(cascadia.QueryAll(root, selector(".dm-message-and-reacts-container")), 0) assert.Len(cascadia.QueryAll(root, selector(".dm-message")), 0)
// Should have the poller at the bottom, with the same value as previously // Should have the poller at the bottom, with the same value as previously
node := cascadia.Query(root, selector("#new-messages-poller")) node := cascadia.Query(root, selector("#new-messages-poller"))

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,6 @@
{{define "error-toast"}} {{define "error-toast"}}
<div class="server-error-msg"> <dialog class="server-error-msg" open>
<div class="error-msg-container">
<span>{{.ErrorMsg}}</span> <span>{{.ErrorMsg}}</span>
<button class="suicide" onclick="htmx.remove('.server-error-msg')">X</button> <button class="suicide" onclick="htmx.remove('.server-error-msg')">X</button>
</div> </dialog>
</div>
{{end}} {{end}}

View File

@ -1,25 +1,16 @@
{{define "author-info"}} {{define "author-info"}}
<div class="author-info" hx-boost="true"> <div class="author-info" hx-boost="true">
<a class="unstyled-link" href="/{{.Handle}}"> {{template "circle-profile-img" .}}
<img <span class="author-info__name-and-handle">
class="profile-image" <div class="author-info__display-name row">
{{if .IsContentDownloaded}}
src="/content/{{.GetProfileImageLocalPath}}"
{{else}}
src="{{.ProfileImageUrl}}"
{{end}}
/>
</a>
<span class="name-and-handle">
<div class="display-name row">
{{.DisplayName}} {{.DisplayName}}
{{if .IsPrivate}} {{if .IsPrivate}}
<div class="circle-outline"> <div class="circle-outline">
<img class="svg-icon" src="/static/icons/lock.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/lock.svg" width="24" height="24">
</div> </div>
{{end}} {{end}}
</div> </div>
<div class="handle">@{{.Handle}}</div> <div class="author-info__handle">@{{.Handle}}</div>
</span> </span>
</div> </div>
{{end}} {{end}}

View File

@ -24,11 +24,11 @@
</head> </head>
<body> <body>
<header class="row search-bar"> <header class="row search-bar">
<a onclick="window.history.back()" class="back-button quick-link"> <a onclick="window.history.back()" class="button search-bar__back-button">
<img class="svg-icon" src="/static/icons/back.svg" width="24" height="24"/> <img class="svg-icon" src="/static/icons/back.svg" width="24" height="24"/>
</a> </a>
<form class="search-form" hx-get="/search" hx-push-url="true" hx-target="body" hx-swap="inner-html show:window:top"> <form class="search-bar__form" hx-get="/search" hx-push-url="true" hx-target="body" hx-swap="inner-html show:window:top">
<input id="search-bar" <input id="search-bar" class="search-bar__input"
name="q" name="q"
placeholder="Search" type="text" placeholder="Search" type="text"
{{with (search_text)}} value="{{.}}" {{end}} {{with (search_text)}} value="{{.}}" {{end}}
@ -39,9 +39,9 @@
<main> <main>
{{template "main" .}} {{template "main" .}}
</main> </main>
<dialog id="image_carousel"> <dialog id="image_carousel" class="image-carousel">
<a class="quick-link close-button" onclick="image_carousel.close()">X</a> <a class="button image-carousel__close-button" onclick="image_carousel.close()">X</a>
<img src=""> <img class="image-carousel__active-image" src="">
</dialog> </dialog>
</body> </body>
</html> </html>

View File

@ -0,0 +1,29 @@
{{define "circle-profile-img"}}
<a class="profile-image" href="/{{.Handle}}">
{{/* TODO: add `width` and `height` attrs to the <img>*/}}
<img class="profile-image__image"
{{if .IsContentDownloaded}}
src="/content/{{.GetProfileImageLocalPath}}"
{{else}}
src="{{.ProfileImageUrl}}"
{{end}}
>
</a>
{{end}}
<!-- TODO: How to use this in a User Feed without a ton of prop-drilling? -->
{{define "circle-profile-img-no-link"}}
<a class="profile-image"
hx-trigger="click consume"
onclick="image_carousel.querySelector('img').src = this.querySelector('img').src; image_carousel.showModal();"
>
{{/* TODO: add `width` and `height` attrs to the <img>*/}}
<img class="profile-image__image"
{{if .IsContentDownloaded}}
src="/content/{{.GetProfileImageLocalPath}}"
{{else}}
src="{{.ProfileImageUrl}}"
{{end}}
>
</a>
{{end}}

View File

@ -1,16 +1,16 @@
{{define "likes-count"}} {{define "likes-count"}}
<div class="interaction-stat" hx-trigger="click consume"> <div class="interactions__stat" hx-trigger="click consume">
{{if .IsLikedByCurrentUser}} {{if .IsLikedByCurrentUser}}
<img class="svg-icon like-icon liked" src="/static/icons/like_filled.svg" width="24" height="24" <img class="svg-icon interactions__like-icon liked" src="/static/icons/like_filled.svg" width="24" height="24"
hx-get="/tweet/{{.ID}}/unlike" hx-get="/tweet/{{.ID}}/unlike"
hx-target="closest .interaction-stat" hx-target="closest .interactions__stat"
hx-push-url="false" hx-push-url="false"
hx-swap="outerHTML focus-scroll:false" hx-swap="outerHTML focus-scroll:false"
/> />
{{else}} {{else}}
<img class="svg-icon like-icon" src="/static/icons/like.svg" width="24" height="24" <img class="svg-icon interactions__like-icon" src="/static/icons/like.svg" width="24" height="24"
hx-get="/tweet/{{.ID}}/like" hx-get="/tweet/{{.ID}}/like"
hx-target="closest .interaction-stat" hx-target="closest .interactions__stat"
hx-push-url="false" hx-push-url="false"
hx-swap="outerHTML focus-scroll:false" hx-swap="outerHTML focus-scroll:false"
/> />

View File

@ -1,74 +1,74 @@
{{define "nav-sidebar"}} {{define "nav-sidebar"}}
<nav id="nav-sidebar"> <nav id="nav-sidebar" class="nav-sidebar">
<div id="logged-in-user-info"> <div id="logged-in-user-info">
<div class="quick-link" hx-get="/login" hx-trigger="click" hx-target="body" hx-push-url="true"> <div class="button row" hx-get="/login" hx-trigger="click" hx-target="body" hx-push-url="true">
{{template "author-info" active_user}} {{template "author-info" active_user}}
<img class="svg-icon" src="/static/icons/dotdotdot.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/dotdotdot.svg" width="24" height="24" />
</div> </div>
</div> </div>
<ul class="quick-links"> <ul class="nav-sidebar__buttons">
<a class="unstyled-link" href="/timeline"> <a href="/timeline">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/home.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/home.svg" width="24" height="24" />
<span>Home</span> <label class="nav-sidebar__button-label">Home</label>
</li> </li>
</a> </a>
<a class="unstyled-link" onclick="document.querySelector('#search-bar').focus()"> <a onclick="document.querySelector('#search-bar').focus()">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/explore.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/explore.svg" width="24" height="24" />
<span>Explore</span> <label class="nav-sidebar__button-label">Explore</label>
</li> </li>
</a> </a>
<a class="unstyled-link" href="#"> <a href="#">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/notifications.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/notifications.svg" width="24" height="24" />
<span>Notifications</span> <label class="nav-sidebar__button-label">Notifications</label>
</li> </li>
</a> </a>
{{if (not (eq (active_user).Handle "[nobody]"))}} {{if (not (eq (active_user).Handle "[nobody]"))}}
<a class="unstyled-link" href="/messages"> <a href="/messages">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/messages.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/messages.svg" width="24" height="24" />
<span>Messages</span> <label class="nav-sidebar__button-label">Messages</label>
</li> </li>
</a> </a>
{{end}} {{end}}
<a class="unstyled-link" href="/lists"> <a href="/lists">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/lists.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/lists.svg" width="24" height="24" />
<span>Lists</span> <label class="nav-sidebar__button-label">Lists</label>
</li> </li>
</a> </a>
<a class="unstyled-link" href="#"> <a href="#">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/bookmarks.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/bookmarks.svg" width="24" height="24" />
<span>Bookmarks</span> <label class="nav-sidebar__button-label">Bookmarks</label>
</li> </li>
</a> </a>
<a class="unstyled-link" href="#"> <a href="#">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/communities.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/communities.svg" width="24" height="24" />
<span>Communities</span> <label class="nav-sidebar__button-label">Communities</label>
</li> </li>
</a> </a>
<a class="unstyled-link" href="#"> <a href="#">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/verified.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/verified.svg" width="24" height="24" />
<span>Verified</span> <label class="nav-sidebar__button-label">Verified</label>
</li> </li>
</a> </a>
{{if (not (eq (active_user).Handle "[nobody]"))}} {{if (not (eq (active_user).Handle "[nobody]"))}}
<a class="unstyled-link" href="/{{(active_user).Handle}}"> <a href="/{{(active_user).Handle}}">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/profile.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/profile.svg" width="24" height="24" />
<span>Profile</span> <label class="nav-sidebar__button-label">Profile</label>
</li> </li>
</a> </a>
{{end}} {{end}}
<a class="unstyled-link" href="#"> <a href="#">
<li class="quick-link"> <li class="button labelled-icon">
<img class="svg-icon" src="/static/icons/more.svg" width="24" height="24"/> <img class="svg-icon" src="/static/icons/more.svg" width="24" height="24"/>
<span>More</span> <label class="nav-sidebar__button-label">More</label>
</li> </li>
</a> </a>
</ul> </ul>

View File

@ -1,64 +1,68 @@
{{define "user-header"}} {{define "user-header"}}
<div class="user-header"> <div class="user-header">
{{if .BannerImageLocalPath}} {{if .BannerImageLocalPath}}
<img class="user-header__profile-banner-image"
{{if .IsContentDownloaded}} {{if .IsContentDownloaded}}
<img class="profile-banner-image" src="/content/profile_images/{{.BannerImageLocalPath}}" /> src="/content/profile_images/{{.BannerImageLocalPath}}"
{{else}} {{else}}
<img class="profile-banner-image" src="{{.BannerImageUrl}}" /> src="{{.BannerImageUrl}}"
{{end}} {{end}}
>
{{end}} {{end}}
<div class="user-header-info-container"> <div class="user-header__info-container">
<div class="row"> <div class="row">
{{template "author-info" .}} {{template "author-info" .}}
{{template "following-button" .}} {{template "following-button" .}}
</div> </div>
<div class="user-bio"> <div class="user-header__bio">
{{template "text-with-entities" .Bio}} {{template "text-with-entities" .Bio}}
</div> </div>
{{if .Location}} {{if .Location}}
<div class="user-location bio-info-with-icon"> <div class="user-header__location labelled-icon">
<img class="svg-icon" src="/static/icons/location.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/location.svg" width="24" height="24" />
<span>{{.Location}}</span> <label>{{.Location}}</label>
</div> </div>
{{end}} {{end}}
{{if .Website}} {{if .Website}}
<div class="user-website bio-info-with-icon"> <div class="user-header__website labelled-icon">
<img class="svg-icon" src="/static/icons/website.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/website.svg" width="24" height="24" />
<a class="unstyled-link" target="_blank" href="{{.Website}}">{{.Website}}</a> <label><a target="_blank" href="{{.Website}}">{{.Website}}</a></label>
</div> </div>
{{end}} {{end}}
<div class="user-join-date bio-info-with-icon"> <div class="user-header__join-date labelled-icon">
<img class="svg-icon" src="/static/icons/calendar.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/calendar.svg" width="24" height="24" />
<span>{{.JoinDate.Time.Format "Jan 2, 2006"}}</span> <label>{{.JoinDate.Time.Format "Jan 2, 2006"}}</label>
</div> </div>
<div class="followers-followees-container row"> <div class="followers-followees row">
<a href="/{{.Handle}}/followers" class="followers-container unstyled-link"> <a href="/{{.Handle}}/followers" class="followers-followees__followers">
<span class="followers-count">{{.FollowersCount}}</span> <span class="followers-followees__count">{{.FollowersCount}}</span>
<span class="followers-label">followers</span> <label>followers</label>
</a> </a>
<a href="/{{.Handle}}/followees" class="followers-container unstyled-link"> <a href="/{{.Handle}}/followees" class="followers-followees__followees">
<span class="following-label">is following</span> <label>is following</label>
<span class="following-count">{{.FollowingCount}}</span> <span class="followers-followees__count">{{.FollowingCount}}</span>
</a> </a>
<div class="spacer"></div> <div class="spacer"></div>
<div class="user-feed-buttons-container"> <div class="row">
<a class="unstyled-link quick-link" target="_blank" href="https://twitter.com/{{.Handle}}" title="Open on twitter.com"> <a class="button" target="_blank" href="https://twitter.com/{{.Handle}}" title="Open on twitter.com">
<img class="svg-icon" src="/static/icons/external-link.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/external-link.svg" width="24" height="24" />
</a> </a>
<a class="unstyled-link quick-link" hx-get="?scrape" hx-target="body" hx-indicator=".user-header" title="Refresh"> <a class="button" hx-get="?scrape" hx-target="body" hx-indicator=".user-header" title="Refresh">
<img class="svg-icon" src="/static/icons/refresh.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/refresh.svg" width="24" height="24" />
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<div class="htmx-spinner-container"> <div class="htmx-spinner">
<div class="htmx-spinner-background"></div> <div class="htmx-spinner__fullscreen-forcer">
<img class="svg-icon htmx-spinner" src="/static/icons/spinner.svg" /> <div class="htmx-spinner__background"></div>
<img class="svg-icon htmx-spinner__icon" src="/static/icons/spinner.svg" />
</div>
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -4,12 +4,12 @@
<div class="list-feed-header"> <div class="list-feed-header">
<h1>{{.List.Name}}</h1> <h1>{{.List.Name}}</h1>
<div class="row tabs-container"> <div class="tabs row">
<a class="tab unstyled-link {{if (eq .ActiveTab "feed")}}active-tab{{end}}" href="/lists/{{.List.ID}}"> <a class="tabs__tab {{if (eq .ActiveTab "feed")}}tabs__tab--active{{end}}" href="/lists/{{.List.ID}}">
<span class="tab-inner">Feed</span> <span class="tabs__tab-label">Feed</span>
</a> </a>
<a class="tab unstyled-link {{if (eq .ActiveTab "users")}}active-tab{{end}}" href="/lists/{{.List.ID}}/users"> <a class="tabs__tab {{if (eq .ActiveTab "users")}}tabs__tab--active{{end}}" href="/lists/{{.List.ID}}/users">
<span class="tab-inner">Users</span> <span class="tabs__tab-label">Users</span>
</a> </a>
</div> </div>
</div> </div>

View File

@ -1,27 +0,0 @@
{{define "title"}}{{.List.Name}}{{end}}
{{define "main"}}
{{$user := (user .UserID)}}
<div class="user-feed-header">
{{template "user-header" $user}}
<div class="row tabs-container">
<a class="tab unstyled-link {{if (eq .FeedType "")}}active-tab{{end}}" href="/{{$user.Handle}}">
<span class="tab-inner">Tweets and replies</span>
</a>
<a class="tab unstyled-link {{if (eq .FeedType "without_replies")}}active-tab{{end}}" href="/{{$user.Handle}}/without_replies">
<span class="tab-inner">Tweets</span>
</a>
<a class="tab unstyled-link {{if (eq .FeedType "media")}}active-tab{{end}}" href="/{{$user.Handle}}/media">
<span class="tab-inner">Media</span>
</a>
<a class="tab unstyled-link {{if (eq .FeedType "likes")}}active-tab{{end}}" href="/{{$user.Handle}}/likes">
<span class="tab-inner">Likes</span>
</a>
</div>
</div>
<div class="timeline user-feed-timeline">
{{template "timeline" .Feed}}
</div>
{{end}}

View File

@ -14,27 +14,18 @@
<button onclick="document.querySelector('#newListDialog').close()">Cancel</button> <button onclick="document.querySelector('#newListDialog').close()">Cancel</button>
</dialog> </dialog>
<div class="users-list-previews"> <div class="list-of-lists">
{{range .}} {{range .}}
{{$max_display_users := 10}} {{$max_display_users := 10}}
<div class="users-list-preview row row--spread"> <div class="list-preview row row--spread">
<div class="list-info-container" hx-get="/lists/{{.ID}}" hx-trigger="click" hx-target="body" hx-push-url="true"> <div class="list-preview__info-container" hx-get="/lists/{{.ID}}" hx-trigger="click" hx-target="body" hx-push-url="true">
<span class="list-name">{{.Name}}</span> <span class="list-name">{{.Name}}</span>
<span class="num-users">({{(len .Users)}})</span> <span class="list-preview__num-users">({{(len .Users)}})</span>
<div class="first-N-profile-images" hx-trigger="click consume"> <div class="list-preview__first-N-profile-images" hx-trigger="click consume">
{{range $i, $user := .Users}} {{range $i, $user := .Users}}
{{/* Only render the first 10-ish users */}} {{/* Only render the first 10-ish users */}}
{{if (lt $i $max_display_users)}} {{if (lt $i $max_display_users)}}
<a class="unstyled-link" href="/{{$user.Handle}}"> {{template "circle-profile-img" $user}}
<img
class="profile-image"
{{if $user.IsContentDownloaded}}
src="/content/{{$user.GetProfileImageLocalPath}}"
{{else}}
src="{{$user.ProfileImageUrl}}"
{{end}}
/>
</a>
{{end}} {{end}}
{{end}} {{end}}
{{if (gt (len .Users) $max_display_users)}} {{if (gt (len .Users) $max_display_users)}}
@ -42,7 +33,7 @@
{{end}} {{end}}
</div> </div>
</div> </div>
<a class="unstyled-link quick-link danger" <a class="button button--danger"
hx-delete="/lists/{{.ID}}" hx-target="body" hx-delete="/lists/{{.ID}}" hx-target="body"
onclick="return confirm('Delete this list? Are you sure?')" onclick="return confirm('Delete this list? Are you sure?')"
>Delete</a> >Delete</a>

View File

@ -1,44 +1,49 @@
{{define "title"}}Login{{end}} {{define "title"}}Login{{end}}
{{define "main"}} {{define "main"}}
<div class="login"> <div class="login-page">
<form hx-post="/change-session" hx-target="#nav-sidebar" hx-swap="outerHTML" hx-ext="json-enc"> <h1>Login</h1>
<label for="select-account">Choose account:</label>
<select name="account" id="select-account"> <form class="choose-session" hx-post="/change-session" hx-target="#nav-sidebar" hx-swap="outerHTML" hx-ext="json-enc">
<h3>Open existing session</h3>
<div class="row row--spread choose-session__form-contents">
<select name="account" class="choose-session__dropdown">
{{range .ExistingSessions}} {{range .ExistingSessions}}
<option value="{{.}}">@{{.}}</option> <option value="{{.}}">@{{.}}</option>
{{end}} {{end}}
<option value="no account">[no account (don't log in)]</option> <option value="no account">[no account (don't log in)]</option>
</select> </select>
<div class="field-container submit-container"> <div class="login-form__field-container login-form__submit-container">
<input type='submit' value='Use account'> <input type='submit' value='Go'>
</div>
</div> </div>
</form> </form>
<p>Or log in</p> <hr>
<form class="login-form" hx-post="/login" hx-target="body" hx-ext="json-enc"> <form class="login-form" hx-post="/login" hx-target="body" hx-ext="json-enc">
<div class="field-container"> <h3>Log in (new session)</h3>
<div class="login-form__field-container">
<label>Username</label> <label>Username</label>
{{with .FormErrors.username}} {{with .FormErrors.username}}
<label class='error'>({{.}})</label> <label class='login-form__error-label'>({{.}})</label>
{{end}} {{end}}
<input name='username' value='{{.Username}}'> <input name='username' value='{{.Username}}' class="login-form__input">
</div> </div>
<div class="field-container"> <div class="login-form__field-container">
<label>Password:</label> <label>Password:</label>
{{with .FormErrors.password}} {{with .FormErrors.password}}
<label class='error'>({{.}})</label> <label class='login-form__error-label'>({{.}})</label>
{{end}} {{end}}
<input type='password' name='password'> <input type='password' name='password' class="login-form__input">
</div> </div>
<div class="field-container submit-container"> <div class="login-form__field-container login-form__submit-container">
<input type='submit' value='Login'> <input type='submit' value='Login'>
</div> </div>
<div class="htmx-spinner-container"> <div class="htmx-spinner">
<div class="htmx-spinner-background"></div> <div class="htmx-spinner__background"></div>
<img class="svg-icon htmx-spinner" src="/static/icons/spinner.svg" /> <img class="svg-icon htmx-spinner__icon" src="/static/icons/spinner.svg" />
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,7 +1,7 @@
{{define "title"}}Messages{{end}} {{define "title"}}Messages{{end}}
{{define "main"}} {{define "main"}}
<div class="chats-container"> <div class="messages-page">
{{template "chat-list" .}} {{template "chat-list" .}}
{{template "chat-view" .}} {{template "chat-view" .}}
</div> </div>

View File

@ -2,12 +2,12 @@
{{define "main"}} {{define "main"}}
<div class="timeline-header"> <div class="timeline-header">
<div class="row tabs-container"> <div class="tabs row">
<a class="tab unstyled-link {{if (eq .ActiveTab "User feed")}}active-tab{{end}}" href="/timeline"> <a class="tabs__tab {{if (eq .ActiveTab "User feed")}}tabs__tab--active{{end}}" href="/timeline">
<span class="tab-inner">User feed</span> <span class="tabs__tab-label">User feed</span>
</a> </a>
<a class="tab unstyled-link {{if (eq .ActiveTab "Offline")}}active-tab{{end}}" href="/timeline/offline"> <a class="tabs__tab {{if (eq .ActiveTab "Offline")}}tabs__tab--active{{end}}" href="/timeline/offline">
<span class="tab-inner">Offline timeline</span> <span class="tabs__tab-label">Offline timeline</span>
</a> </a>
</div> </div>
</div> </div>

View File

@ -5,35 +5,35 @@
<div class="row row--spread"> <div class="row row--spread">
<div class="dummy"></div> {{/* Extra div to take up a slot in the `row` */}} <div class="dummy"></div> {{/* Extra div to take up a slot in the `row` */}}
<h1>Search results: {{.SearchText}}</h1> <h1>Search results: {{.SearchText}}</h1>
<div class="user-feed-buttons-container"> <div class="row">
<a class="unstyled-link quick-link" target="_blank" href="https://twitter.com/search?q={{.SearchText}}&src=typed_query&f=top" title="Open on twitter.com"> <a class="button" target="_blank" href="https://twitter.com/search?q={{.SearchText}}&src=typed_query&f=top" title="Open on twitter.com">
<img class="svg-icon" src="/static/icons/external-link.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/external-link.svg" width="24" height="24" />
</a> </a>
<a class="unstyled-link quick-link" hx-get="?scrape" hx-target="body" hx-indicator=".search-header" title="Refresh"> <a class="button" hx-get="?scrape" hx-target="body" hx-indicator=".search-header" title="Refresh">
<img class="svg-icon" src="/static/icons/refresh.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/refresh.svg" width="24" height="24" />
</a> </a>
</div> </div>
</div> </div>
<div class="row tabs-container"> <div class="tabs row">
<a class="tab unstyled-link {{if (not .IsUsersSearch)}}active-tab{{end}}" href="?type=tweets"> <a class="tabs__tab {{if (not .IsUsersSearch)}}tabs__tab--active{{end}}" href="?type=tweets">
<span class="tab-inner">Tweets</span> <span class="tabs__tab-label">Tweets</span>
</a> </a>
<a class="tab unstyled-link {{if .IsUsersSearch}}active-tab{{end}}" href="?type=users"> <a class="tabs__tab {{if .IsUsersSearch}}tabs__tab--active{{end}}" href="?type=users">
<span class="tab-inner">Users</span> <span class="tabs__tab-label">Users</span>
</a> </a>
</div> </div>
<div class="htmx-spinner-container"> <div class="htmx-spinner">
<div class="htmx-spinner-background"></div> <div class="htmx-spinner__background"></div>
<img class="svg-icon htmx-spinner" src="/static/icons/spinner.svg" /> <img class="svg-icon htmx-spinner__icon" src="/static/icons/spinner.svg" />
</div> </div>
</div> </div>
{{if .IsUsersSearch}} {{if .IsUsersSearch}}
{{template "list" (dict "UserIDs" .UserIDs)}} {{template "list" (dict "UserIDs" .UserIDs)}}
{{else}} {{else}}
<div class="sort-order-container"> <div class="sort-order">
<span class="sort-order-label">order:</span> <label class="sort-order__label">order:</label>
<select name="sort-order" hx-get="#" hx-target="body" hx-push-url="true"> <select class="sort-order__dropdown" name="sort-order" hx-get="#" hx-target="body" hx-push-url="true">
{{range .SortOrderOptions}} {{range .SortOrderOptions}}
<option <option
value="{{.}}" value="{{.}}"

View File

@ -2,12 +2,14 @@
{{define "main"}} {{define "main"}}
<div class="tweet-detail">
{{range .ParentIDs}} {{range .ParentIDs}}
<div class="thread-parent-tweet"> <div class="thread-parent-tweet">
{{template "tweet" (dict "TweetID" . "RetweetID" 0 "QuoteNestingLevel" 0)}} {{template "tweet" (dict "TweetID" . "RetweetID" 0 "QuoteNestingLevel" 0)}}
</div> </div>
{{end}} {{end}}
<div id="focused-tweet">
<div id="focused-tweet" class="focused-tweet">
{{template "tweet" (dict "TweetID" .MainTweetID "RetweetID" 0 "QuoteNestingLevel" 0)}} {{template "tweet" (dict "TweetID" .MainTweetID "RetweetID" 0 "QuoteNestingLevel" 0)}}
</div> </div>
@ -30,4 +32,5 @@
{{end}} {{end}}
</div> </div>
{{end}} {{end}}
</div>
{{end}} {{end}}

View File

@ -7,7 +7,7 @@
{{/* Scroll the active chat into view, if there is one */}} {{/* Scroll the active chat into view, if there is one */}}
{{if $.ActiveRoomID}} {{if $.ActiveRoomID}}
<script> <script>
document.querySelector(".chat.active-chat").scrollIntoViewIfNeeded(true) document.querySelector(".chat-list-entry.chat-list-entry--active-chat").scrollIntoViewIfNeeded(true)
</script> </script>
{{end}} {{end}}
</div> </div>

View File

@ -1,7 +1,12 @@
{{define "chat-list-entry"}} {{define "chat-list-entry"}}
{{$room := $.room}} {{$room := $.room}}
<div class="chat {{if .is_active}}active-chat{{end}}" hx-get="/messages/{{$room.ID}}" hx-push-url="true" hx-swap="outerHTML" hx-target="body"> <div class="chat-list-entry {{if .is_active}}chat-list-entry--active-chat{{end}}"
<div class="chat-preview-header"> hx-get="/messages/{{$room.ID}}"
hx-push-url="true"
hx-swap="outerHTML"
hx-target="body"
>
<div class="chat-list-entry__header">
{{if (eq $room.Type "ONE_TO_ONE")}} {{if (eq $room.Type "ONE_TO_ONE")}}
{{range $room.Participants}} {{range $room.Participants}}
{{if (ne .UserID (active_user).ID)}} {{if (ne .UserID (active_user).ID)}}
@ -12,19 +17,21 @@
{{end}} {{end}}
{{end}} {{end}}
{{else}} {{else}}
<div class="groupchat-profile-image-container"> <div class="chat-list-entry__groupchat-profile-image">
<img class="profile-image" src="{{$room.AvatarImageRemoteURL}}" width="48" height="48" /> {{template "circle-profile-img-no-link" (dict "IsContentDownloaded" false "ProfileImageUrl" $room.AvatarImageRemoteURL)}}
<div class="click-eater" hx-trigger="click consume" hx-target="body">
<div class="display-name row">{{$room.Name}}</div> <div class="display-name row">{{$room.Name}}</div>
</div> </div>
</div>
{{end}} {{end}}
<div class="chat-preview-timestamp .posted-at-container"> <div class="posted-at">
<p class="posted-at"> <p class="posted-at__text">
{{$room.LastMessagedAt.Time.Format "Jan 2, 2006"}} {{$room.LastMessagedAt.Time.Format "Jan 2, 2006"}}
<br/> <br/>
{{$room.LastMessagedAt.Time.Format "3:04 pm"}} {{$room.LastMessagedAt.Time.Format "3:04 pm"}}
</p> </p>
</div> </div>
</div> </div>
<p class="chat-preview">{{(index $.messages $room.LastMessageID).Text}}</p> <p class="chat-list-entry__message-preview">{{(index $.messages $room.LastMessageID).Text}}</p>
</div> </div>
{{end}} {{end}}

View File

@ -1,25 +1,19 @@
{{define "messages-with-poller"}} {{define "messages"}}
{{range .MessageIDs}} {{range .MessageIDs}}
{{$message := (index $.DMTrove.Messages .)}} {{$message := (index $.DMTrove.Messages .)}}
{{$user := (user $message.SenderID)}} {{$user := (user $message.SenderID)}}
{{$is_us := (eq $message.SenderID (active_user).ID)}} {{$is_us := (eq $message.SenderID (active_user).ID)}}
<div class="dm-message-and-reacts-container {{if $is_us}} our-message {{end}}"> <div class="dm-message {{if $is_us}} our-message {{end}}">
<div class="dm-message-container"> <div class="dm-message__row row">
<div class="sender-profile-image-container"> <div class="dm-message__sender-profile-img">
<a class="unstyled-link" href="/{{$user.Handle}}"> {{template "circle-profile-img" $user}}
{{if $user.IsContentDownloaded}}
<img class="profile-image" src="/content/{{$user.GetProfileImageLocalPath}}" />
{{else}}
<img class="profile-image" src="{{$user.ProfileImageUrl}}" />
{{end}}
</a>
</div> </div>
<div class="dm-message-content-container"> <div class="dm-message__contents">
{{if (ne $message.InReplyToID 0)}} {{if (ne $message.InReplyToID 0)}}
<div class="replying-to-container"> <div class="dm-message__replying-to">
<div class="replying-to-label row"> <div class="dm-message__replying-to-label labelled-icon">
<img class="svg-icon" src="/static/icons/replying_to.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/replying_to.svg" width="24" height="24" />
<span>Replying to</span> <label>Replying to</label>
</div> </div>
<div class="replying-to-message"> <div class="replying-to-message">
{{(index $.DMTrove.Messages $message.InReplyToID).Text}} {{(index $.DMTrove.Messages $message.InReplyToID).Text}}
@ -27,7 +21,7 @@
</div> </div>
{{end}} {{end}}
{{if (ne $message.EmbeddedTweetID 0)}} {{if (ne $message.EmbeddedTweetID 0)}}
<div class="tweet-preview"> <div class="dm-message__tweet-preview">
{{template "tweet" (dict {{template "tweet" (dict
"TweetID" $message.EmbeddedTweetID "TweetID" $message.EmbeddedTweetID
"RetweetID" 0 "RetweetID" 0
@ -36,7 +30,7 @@
</div> </div>
{{end}} {{end}}
{{range $message.Images}} {{range $message.Images}}
<img class="dm-embedded-image" <img class="dm-message__embedded-image"
{{if .IsDownloaded}} {{if .IsDownloaded}}
src="/content/images/{{.LocalFilename}}" src="/content/images/{{.LocalFilename}}"
{{else}} {{else}}
@ -47,7 +41,7 @@
> >
{{end}} {{end}}
{{range $message.Videos}} {{range $message.Videos}}
<video controls width="{{.Width}}" height="{{.Height}}" <video class="dm-message__embedded-video" controls width="{{.Width}}" height="{{.Height}}"
{{if .IsDownloaded}} {{if .IsDownloaded}}
poster="/content/video_thumbnails/{{.ThumbnailLocalPath}}" poster="/content/video_thumbnails/{{.ThumbnailLocalPath}}"
{{else}} {{else}}
@ -62,47 +56,32 @@
</video> </video>
{{end}} {{end}}
{{range $message.Urls}} {{range $message.Urls}}
<a {{template "embedded-link" .}}
class="embedded-link rounded-gray-outline unstyled-link"
target="_blank"
href="{{.Text}}"
style="max-width: {{if (ne .ThumbnailWidth 0)}}{{.ThumbnailWidth}}px {{else}}fit-content {{end}}"
>
<img
{{if .IsContentDownloaded}}
src="/content/link_preview_images/{{.ThumbnailLocalPath}}"
{{else}}
src="{{.ThumbnailRemoteUrl}}"
{{end}}
class="embedded-link-preview"
width="{{.ThumbnailWidth}}" height="{{.ThumbnailHeight}}"
/>
<h3 class="embedded-link-title">{{.Title}}</h3>
<p class="embedded-link-description">{{.Description}}</p>
<span class="row embedded-link-domain-container">
<img class="svg-icon" src="/static/icons/link3.svg" width="24" height="24" />
<span class="embedded-link-domain">{{(.GetDomain)}}</span>
</span>
</a>
{{end}} {{end}}
{{if $message.Text}} {{if $message.Text}}
<div class="dm-message-text-container"> <div class="dm-message__text-content">
{{template "text-with-entities" $message.Text}} {{template "text-with-entities" $message.Text}}
</div> </div>
{{end}} {{end}}
</div> </div>
</div> </div>
<div class="dm-message-reactions"> <div class="dm-message__reactions">
{{range $message.Reactions}} {{range $message.Reactions}}
{{$sender := (user .SenderID)}} {{$sender := (user .SenderID)}}
<span title="{{$sender.DisplayName}} (@{{$sender.Handle}})">{{.Emoji}}</span> <span title="{{$sender.DisplayName}} (@{{$sender.Handle}})">{{.Emoji}}</span>
{{end}} {{end}}
</div> </div>
<p class="posted-at"> <div class="sent-at">
<p class="sent-at__text">
{{$message.SentAt.Time.Format "Jan 2, 2006 @ 3:04 pm"}} {{$message.SentAt.Time.Format "Jan 2, 2006 @ 3:04 pm"}}
</p> </p>
</div> </div>
</div>
{{end}} {{end}}
{{end}}
{{define "messages-with-poller"}}
{{template "messages" .}}
<div id="new-messages-poller" <div id="new-messages-poller"
hx-swap="outerHTML {{if $.ScrollBottom}}scroll:.chat-messages:bottom{{end}}" hx-swap="outerHTML {{if $.ScrollBottom}}scroll:.chat-messages:bottom{{end}}"
@ -119,17 +98,21 @@
{{end}} {{end}}
</div> </div>
{{if $.ActiveRoomID}} {{if $.ActiveRoomID}}
<div class="dm-composer-container"> <div class="dm-composer">
<form hx-post="/messages/{{$.ActiveRoomID}}/send" hx-target="#new-messages-poller" hx-swap="outerHTML scroll:.chat-messages:bottom" hx-ext="json-enc"> <form
hx-post="/messages/{{$.ActiveRoomID}}/send"
hx-target="#new-messages-poller"
hx-swap="outerHTML scroll:.chat-messages:bottom"
hx-ext="json-enc"
>
{{template "dm-composer"}} {{template "dm-composer"}}
<input id="real-input" type="hidden" name="text" value="" /> <input id="realInput" type="hidden" name="text" value="" />
<input type="submit" /> <input type="submit" />
</form> </form>
</div> </div>
<script> <script>
// Make pasting text work for HTML as well as plain text // Make pasting text work for HTML as well as plain text
var editor = document.querySelector("#composer"); composer.addEventListener("paste", function(e) {
editor.addEventListener("paste", function(e) {
// cancel paste // cancel paste
e.preventDefault(); e.preventDefault();
// get text representation of clipboard // get text representation of clipboard
@ -180,10 +163,7 @@
{{end}} {{end}}
{{define "dm-composer"}} {{define "dm-composer"}}
<span <span id="composer" role="textbox" contenteditable oninput="realInput.value = this.innerText"
id="composer"
role="textbox"
contenteditable
{{if .}} {{if .}}
{{/* {{/*
This is a separate template so it can be OOB-swapped to clear the contents of the composer This is a separate template so it can be OOB-swapped to clear the contents of the composer
@ -196,7 +176,5 @@
*/}} */}}
hx-swap-oob="true" hx-swap-oob="true"
{{end}} {{end}}
oninput="var text = this.innerText; document.querySelector('#real-input').value = text" ></span>
>
</span>
{{end}} {{end}}

View File

@ -0,0 +1,24 @@
{{define "embedded-link"}}
<a
class="embedded-link rounded-gray-outline"
target="_blank"
href="{{.Text}}"
style="max-width: {{if (ne .ThumbnailWidth 0)}}{{.ThumbnailWidth}}px {{else}}fit-content {{end}}"
>
<img
{{if .IsContentDownloaded}}
src="/content/link_preview_images/{{.ThumbnailLocalPath}}"
{{else}}
src="{{.ThumbnailRemoteUrl}}"
{{end}}
class="embedded-link__preview-image"
width="{{.ThumbnailWidth}}" height="{{.ThumbnailHeight}}"
/>
<h3 class="embedded-link__title">{{.Title}}</h3>
<p class="embedded-link__description">{{.Description}}</p>
<span class="row embedded-link__domain">
<img class="svg-icon" src="/static/icons/link3.svg" width="24" height="24" />
<span class="embedded-link__domain__contents">{{(.GetDomain)}}</span>
</span>
</a>
{{end}}

View File

@ -1,12 +1,16 @@
{{define "list"}} {{define "list"}}
<div class="users-list-container"> <div class="users-list">
{{range .UserIDs}} {{range .UserIDs}}
{{$user := (user .)}} {{$user := (user .)}}
<div class="user"> <div class="user">
<div class="row row--spread"> <div class="row row--spread">
{{template "author-info" $user}} {{template "author-info" $user}}
{{if $.button_text}} {{if $.button_text}}
<a class="unstyled-link quick-link danger" href="{{$.button_url}}?user_handle={{$user.Handle}}"onclick="return confirm('{{$.button_text}} this user? Are you sure?')"> <a
href="{{$.button_url}}?user_handle={{$user.Handle}}"
class="button button--danger"
onclick="return confirm('{{$.button_text}} this user? Are you sure?')"
>
{{$.button_text}} {{$.button_text}}
</a> </a>
{{end}} {{end}}

View File

@ -1,9 +1,9 @@
{{define "poll-choice"}} {{define "poll-choice"}}
<div class="row poll-choice"> <div class="row poll__choice">
<div class="poll-fill-bar {{if (.poll.IsWinner .votes)}}poll-winner{{end}}" style="width: {{printf "%.1f" (.poll.VotePercentage .votes)}}%"></div> <div class="poll__choice-fill-bar {{if (.poll.IsWinner .votes)}}poll__choice-fill-bar--winner{{end}}" style="width: {{printf "%.1f" (.poll.VotePercentage .votes)}}%"></div>
<div class="poll-info-container row"> <div class="poll__choice-info row">
<span class="poll-choice-label">{{.label}}</span> <span class="poll__choice-label">{{.label}}</span>
<span class="poll-choice-votes">{{.votes}} ({{printf "%.1f" (.poll.VotePercentage .votes)}}%)</span> <span class="poll__choice-votes">{{.votes}} ({{printf "%.1f" (.poll.VotePercentage .votes)}}%)</span>
</div> </div>
</div> </div>
@ -21,8 +21,8 @@
{{template "poll-choice" (dict "label" .Choice4 "votes" .Choice4_Votes "poll" .)}} {{template "poll-choice" (dict "label" .Choice4 "votes" .Choice4_Votes "poll" .)}}
{{end}} {{end}}
<p class="poll-metadata"> <p class="poll__metadata">
<span class="poll-state"> <span class="poll__metadata__state">
{{if .IsOpen}} {{if .IsOpen}}
Poll open, voting ends at {{.FormatEndsAt}} Poll open, voting ends at {{.FormatEndsAt}}
{{else}} {{else}}

View File

@ -13,30 +13,36 @@
{{if (not (eq .RetweetID 0))}} {{if (not (eq .RetweetID 0))}}
{{$retweet := (retweet .RetweetID)}} {{$retweet := (retweet .RetweetID)}}
{{$retweet_user := (user $retweet.RetweetedByID)}} {{$retweet_user := (user $retweet.RetweetedByID)}}
<div class="retweet-info-container" hx-trigger="click consume"> <div class="retweet-info" hx-trigger="click consume">
<img class="svg-icon" src="/static/icons/retweet.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/retweet.svg" width="24" height="24" />
<span class="retweeted-by-label">Retweeted by</span> <span class="retweet-info__retweeted-by-label">Retweeted by</span>
<a class="retweeted-by-user" hx-get="/{{$retweet_user.Handle}}" hx-target="body" hx-swap="outerHTML" hx-push-url="true"> <a
class="retweet-info__retweeted-by-user"
hx-get="/{{$retweet_user.Handle}}"
hx-target="body"
hx-swap="outerHTML"
hx-push-url="true"
>
{{$retweet_user.DisplayName}} {{$retweet_user.DisplayName}}
</a> </a>
</div> </div>
{{end}} {{end}}
<div class="tweet-header-container"> <div class="tweet__header-container">
<div class="author-info-container" hx-trigger="click consume"> <div class="author-info-container" hx-trigger="click consume">
{{template "author-info" $author}} {{template "author-info" $author}}
</div> </div>
{{if $main_tweet.ReplyMentions}} {{if $main_tweet.ReplyMentions}}
<div class="reply-mentions-container" hx-trigger="click consume"> <div class="reply-mentions" hx-trigger="click consume">
<span class="replying-to-label">Replying&nbsp;to</span> <span class="reply-mentions__dm-message__replying-to-label">Replying&nbsp;to</span>
<ul class="reply-mentions inline-dotted-list"> <ul class="reply-mentions__list inline-dotted-list">
{{range $main_tweet.ReplyMentions}} {{range $main_tweet.ReplyMentions}}
<li><a class="entity" href="/{{.}}">@{{.}}</a></li> <li><a class="entity" href="/{{.}}">@{{.}}</a></li>
{{end}} {{end}}
</ul> </ul>
</div> </div>
{{end}} {{end}}
<div class="posted-at-container"> <div class="posted-at">
<p class="posted-at"> <p class="posted-at__text">
{{$main_tweet.PostedAt.Time.Format "Jan 2, 2006"}} {{$main_tweet.PostedAt.Time.Format "Jan 2, 2006"}}
<br/> <br/>
{{$main_tweet.PostedAt.Time.Format "3:04 pm"}} {{$main_tweet.PostedAt.Time.Format "3:04 pm"}}
@ -44,11 +50,11 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<span class="vertical-reply-line-container"> <span class="string-box">
<div class="vertical-reply-line"> <div class="string">
</div> </div>
</span> </span>
<span class="vertical-container-1"> <span class="tweet__vertical-container">
<div class="tweet-content"> <div class="tweet-content">
{{if (ne $main_tweet.TombstoneType "")}} {{if (ne $main_tweet.TombstoneType "")}}
<div class="tombstone"> <div class="tombstone">
@ -57,7 +63,7 @@
{{end}} {{end}}
{{template "text-with-entities" $main_tweet.Text}} {{template "text-with-entities" $main_tweet.Text}}
{{range $main_tweet.Images}} {{range $main_tweet.Images}}
<img class="tweet-image" <img class="tweet__embedded-image"
{{if .IsDownloaded}} {{if .IsDownloaded}}
src="/content/images/{{.LocalFilename}}" src="/content/images/{{.LocalFilename}}"
{{else}} {{else}}
@ -69,7 +75,7 @@
{{end}} {{end}}
hx-trigger="click consume" hx-trigger="click consume"
onclick="image_carousel.querySelector('img').src = this.src; image_carousel.showModal();" onclick="image_carousel.querySelector('img').src = this.src; image_carousel.showModal();"
/> >
{{end}} {{end}}
{{range $main_tweet.Videos}} {{range $main_tweet.Videos}}
<video controls hx-trigger="click consume" width="{{.Width}}" height="{{.Height}}" <video controls hx-trigger="click consume" width="{{.Width}}" height="{{.Height}}"
@ -88,28 +94,7 @@
{{end}} {{end}}
{{range $main_tweet.Urls}} {{range $main_tweet.Urls}}
<div class="click-eater" hx-trigger="click consume"> <div class="click-eater" hx-trigger="click consume">
<a {{template "embedded-link" .}}
class="embedded-link rounded-gray-outline unstyled-link"
target="_blank"
href="{{.Text}}"
style="max-width: {{if (ne .ThumbnailWidth 0)}}{{.ThumbnailWidth}}px {{else}}fit-content {{end}}"
>
<img
{{if .IsContentDownloaded}}
src="/content/link_preview_images/{{.ThumbnailLocalPath}}"
{{else}}
src="{{.ThumbnailRemoteUrl}}"
{{end}}
class="embedded-link-preview"
width="{{.ThumbnailWidth}}" height="{{.ThumbnailHeight}}"
/>
<h3 class="embedded-link-title">{{.Title}}</h3>
<p class="embedded-link-description">{{.Description}}</p>
<span class="row embedded-link-domain-container">
<img class="svg-icon" src="/static/icons/link3.svg" width="24" height="24" />
<span class="embedded-link-domain">{{(.GetDomain)}}</span>
</span>
</a>
</div> </div>
{{end}} {{end}}
{{range $main_tweet.Polls}} {{range $main_tweet.Polls}}
@ -117,8 +102,12 @@
{{end}} {{end}}
{{if (and $main_tweet.QuotedTweetID (lt .QuoteNestingLevel 1))}} {{if (and $main_tweet.QuotedTweetID (lt .QuoteNestingLevel 1))}}
<div class="quoted-tweet rounded-gray-outline" hx-trigger="click consume"> <div class="tweet__quoted-tweet rounded-gray-outline" hx-trigger="click consume">
{{template "tweet" (dict "TweetID" $main_tweet.QuotedTweetID "RetweetID" 0 "QuoteNestingLevel" (add .QuoteNestingLevel 1))}} {{template "tweet" (dict
"TweetID" $main_tweet.QuotedTweetID
"RetweetID" 0
"QuoteNestingLevel" (add .QuoteNestingLevel 1)
) }}
</div> </div>
{{end}} {{end}}
{{if $main_tweet.SpaceID}} {{if $main_tweet.SpaceID}}
@ -126,35 +115,46 @@
{{end}} {{end}}
</div> </div>
<div class="interactions-bar row"> <div class="interactions row">
<div class="interaction-stat"> <div class="interactions__stat">
<img class="svg-icon" src="/static/icons/quote.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/quote.svg" width="24" height="24" />
<span>{{$main_tweet.NumQuoteTweets}}</span> <span>{{$main_tweet.NumQuoteTweets}}</span>
</div> </div>
<div class="interaction-stat"> <div class="interactions__stat">
<img class="svg-icon" src="/static/icons/reply.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/reply.svg" width="24" height="24" />
<span>{{$main_tweet.NumReplies}}</span> <span>{{$main_tweet.NumReplies}}</span>
</div> </div>
<div class="interaction-stat"> <div class="interactions__stat">
<img class="svg-icon" src="/static/icons/retweet.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/retweet.svg" width="24" height="24" />
<span>{{$main_tweet.NumRetweets}}</span> <span>{{$main_tweet.NumRetweets}}</span>
</div> </div>
{{template "likes-count" $main_tweet}} {{template "likes-count" $main_tweet}}
<div class="dummy"></div> <div class="interactions__dummy"></div>
<div class="tweet-buttons-container" hx-trigger="click consume"> <div class="row" hx-trigger="click consume">
<a class="unstyled-link quick-link" target="_blank" href="https://twitter.com/{{$author.Handle}}/status/{{$main_tweet.ID}}" title="Open on twitter.com"> <a
class="button"
target="_blank"
href="https://twitter.com/{{$author.Handle}}/status/{{$main_tweet.ID}}"
title="Open on twitter.com"
>
<img class="svg-icon" src="/static/icons/external-link.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/external-link.svg" width="24" height="24" />
</a> </a>
<a class="unstyled-link quick-link" hx-get="/tweet/{{$main_tweet.ID}}?scrape" hx-target="body" title="Refresh"> <a
class="button"
hx-get="/tweet/{{$main_tweet.ID}}?scrape"
hx-target="body"
hx-indicator="closest .tweet"
title="Refresh"
>
<img class="svg-icon" src="/static/icons/refresh.svg" width="24" height="24" /> <img class="svg-icon" src="/static/icons/refresh.svg" width="24" height="24" />
</a> </a>
</div> </div>
</div> </div>
</span> </span>
</div> </div>
<div class="htmx-spinner-container"> <div class="htmx-spinner">
<div class="htmx-spinner-background"></div> <div class="htmx-spinner__background"></div>
<img class="svg-icon htmx-spinner" src="/static/icons/spinner.svg" /> <img class="svg-icon htmx-spinner__icon" src="/static/icons/spinner.svg" />
</div> </div>
</div> </div>
{{end}} {{end}}

View File

@ -1,18 +1,18 @@
{{define "space"}} {{define "space"}}
<div class="space"> <div class="space">
<div class="space-host row"> <div class="space__host row">
{{template "author-info" (user .CreatedById)}} {{template "author-info" (user .CreatedById)}}
<span class="host-label">(Host)</span> <span class="space__host__label">(Host)</span>
<div class="layout-spacer"></div> <div class="space__layout-spacer"></div>
<div class="space-date"> <div class="space__date">
{{.StartedAt.Format "Jan 2, 2006"}}<br>{{.StartedAt.Format "3:04pm"}} {{.StartedAt.Format "Jan 2, 2006"}}<br>{{.StartedAt.Format "3:04pm"}}
</div> </div>
</div> </div>
<h3 class="space-title">{{.Title}}</h3> <h3 class="space__title">{{.Title}}</h3>
<div class="space-info row"> <div class="space__info row">
<span class="space-state"> <span class="space-state">
{{if (eq .State "Ended")}} {{if (eq .State "Ended")}}
<ul class="space-info-list inline-dotted-list"> <ul class="space__info__list inline-dotted-list">
<li>{{.State}}</li> <li>{{.State}}</li>
<li>{{(len .ParticipantIds)}} participants</li> <li>{{(len .ParticipantIds)}} participants</li>
<li>{{.LiveListenersCount}} tuned in</li> <li>{{.LiveListenersCount}} tuned in</li>
@ -23,7 +23,7 @@
{{end}} {{end}}
</span> </span>
</div> </div>
<ul class="space-participants-list"> <ul class="space__participants-list">
{{range .ParticipantIds}} {{range .ParticipantIds}}
{{if (ne . $.CreatedById)}} {{if (ne . $.CreatedById)}}
<li>{{template "author-info" (user .)}}</li> <li>{{template "author-info" (user .)}}</li>

View File

@ -3,5 +3,15 @@
{{template "tweet" .}} {{template "tweet" .}}
{{end}} {{end}}
{{template "timeline-bottom" .CursorBottom}} <div class="timeline__bottom">
{{if .CursorBottom.CursorPosition.IsEnd}}
<label class="timeline__eof-label">End of feed</label>
{{else}}
<a class="timeline__show-more-button button"
hx-get="?{{(cursor_to_query_params .CursorBottom)}}"
hx-target=".timeline__bottom"
hx-swap="outerHTML"
>Show more</a>
{{end}}
</div>
{{end}} {{end}}

View File

@ -1,13 +0,0 @@
{{define "timeline-bottom"}}
<div class="timeline-bottom-container">
{{if .CursorPosition.IsEnd}}
<div class="eof-indicator">End of feed</div>
{{else}}
<a class="show-more quick-link unstyled-link"
hx-get="?{{(cursor_to_query_params .)}}"
hx-target=".timeline-bottom-container"
hx-swap="outerHTML"
>Show more</a>
{{end}}
</div>
{{end}}

View File

@ -5,18 +5,18 @@
<div class="user-feed-header"> <div class="user-feed-header">
{{template "user-header" $user}} {{template "user-header" $user}}
<div class="row tabs-container"> <div class="tabs row">
<a class="tab unstyled-link {{if (eq .FeedType "")}}active-tab{{end}}" href="/{{$user.Handle}}"> <a class="tabs__tab {{if (eq .FeedType "")}}tabs__tab--active{{end}}" href="/{{$user.Handle}}">
<span class="tab-inner">Tweets and replies</span> <span class="tabs__tab-label">Tweets and replies</span>
</a> </a>
<a class="tab unstyled-link {{if (eq .FeedType "without_replies")}}active-tab{{end}}" href="/{{$user.Handle}}/without_replies"> <a class="tabs__tab {{if (eq .FeedType "without_replies")}}tabs__tab--active{{end}}" href="/{{$user.Handle}}/without_replies">
<span class="tab-inner">Tweets</span> <span class="tabs__tab-label">Tweets</span>
</a> </a>
<a class="tab unstyled-link {{if (eq .FeedType "media")}}active-tab{{end}}" href="/{{$user.Handle}}/media"> <a class="tabs__tab {{if (eq .FeedType "media")}}tabs__tab--active{{end}}" href="/{{$user.Handle}}/media">
<span class="tab-inner">Media</span> <span class="tabs__tab-label">Media</span>
</a> </a>
<a class="tab unstyled-link {{if (eq .FeedType "likes")}}active-tab{{end}}" href="/{{$user.Handle}}/likes"> <a class="tabs__tab {{if (eq .FeedType "likes")}}tabs__tab--active{{end}}" href="/{{$user.Handle}}/likes">
<span class="tab-inner">Likes</span> <span class="tabs__tab-label">Likes</span>
</a> </a>
</div> </div>
</div> </div>
@ -24,9 +24,9 @@
<div class="timeline user-feed-timeline"> <div class="timeline user-feed-timeline">
{{if .PinnedTweet.ID}} {{if .PinnedTweet.ID}}
<div class="pinned-tweet"> <div class="pinned-tweet">
<div class="row pinned-tweet__pin-container"> <div class="pinned-tweet__pin-container labelled-icon">
<img class="svg-icon pinned-tweet__pin-icon" src="/static/icons/pin.svg" width="24" height="24" /> <img class="svg-icon pinned-tweet__pin-icon" src="/static/icons/pin.svg" width="24" height="24" />
<span>Pinned</span> <label>Pinned</label>
</div> </div>
{{template "tweet" (dict "TweetID" .PinnedTweet.ID "RetweetID" 0 "QuoteNestingLevel" 0)}} {{template "tweet" (dict "TweetID" .PinnedTweet.ID "RetweetID" 0 "QuoteNestingLevel" 0)}}
</div> </div>