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:
parent
aeb2782356
commit
8410182129
@ -95,7 +95,7 @@ func TestUserFeedWithEntityInBio(t *testing.T) {
|
||||
|
||||
root, err := html.Parse(resp.Body)
|
||||
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)
|
||||
assert.Equal(bio_entities[0].FirstChild.Data, "@SheathUnderwear")
|
||||
}
|
||||
@ -192,7 +192,7 @@ func TestUserFollowers(t *testing.T) {
|
||||
|
||||
root, err := html.Parse(resp.Body)
|
||||
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) {
|
||||
@ -204,7 +204,7 @@ func TestUserFollowees(t *testing.T) {
|
||||
|
||||
root, err := html.Parse(resp.Body)
|
||||
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
|
||||
@ -363,7 +363,7 @@ func TestSearchUsers(t *testing.T) {
|
||||
require.Equal(resp.StatusCode, 200)
|
||||
root, err := html.Parse(resp.Body)
|
||||
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.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)
|
||||
require.NoError(err)
|
||||
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
|
||||
resp = do_request(httptest.NewRequest("GET", "/tweet/1453461248142495744", nil))
|
||||
@ -478,7 +478,7 @@ func TestTweetsWithContent(t *testing.T) {
|
||||
root, err = html.Parse(resp.Body)
|
||||
require.NoError(err)
|
||||
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) {
|
||||
@ -621,7 +621,7 @@ func TestListsIndex(t *testing.T) {
|
||||
require.NoError(err)
|
||||
|
||||
// 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) {
|
||||
@ -633,7 +633,7 @@ func TestListDetail(t *testing.T) {
|
||||
require.Equal(resp.StatusCode, 200)
|
||||
root, err := html.Parse(resp.Body)
|
||||
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
|
||||
resp1 := do_request(httptest.NewRequest("GET", "/lists/2", nil))
|
||||
@ -662,7 +662,7 @@ func TestListAddAndDeleteUser(t *testing.T) {
|
||||
require.Equal(resp.StatusCode, 200)
|
||||
root, err := html.Parse(resp.Body)
|
||||
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
|
||||
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)
|
||||
root, err = html.Parse(resp.Body)
|
||||
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
|
||||
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)
|
||||
root, err = html.Parse(resp.Body)
|
||||
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) {
|
||||
@ -698,7 +698,7 @@ func TestCreateNewList(t *testing.T) {
|
||||
require.Equal(resp.StatusCode, 200)
|
||||
root, err := html.Parse(resp.Body)
|
||||
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
|
||||
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)
|
||||
root, err = html.Parse(resp.Body)
|
||||
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
|
||||
@ -732,8 +732,8 @@ func TestMessagesIndexPage(t *testing.T) {
|
||||
resp := recorder.Result()
|
||||
root, err := html.Parse(resp.Body)
|
||||
require.NoError(err)
|
||||
assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat")), 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-list .chat-list-entry")), 2)
|
||||
assert.Len(cascadia.QueryAll(root, selector(".chat-view .dm-message")), 0) // No messages until you click on one
|
||||
}
|
||||
|
||||
// Open a chat room
|
||||
@ -752,8 +752,8 @@ func TestMessagesRoom(t *testing.T) {
|
||||
resp := recorder.Result()
|
||||
root, err := html.Parse(resp.Body)
|
||||
require.NoError(err)
|
||||
assert.Len(cascadia.QueryAll(root, selector(".chat-list .chat")), 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-list .chat-list-entry")), 2) // Chat list still renders
|
||||
assert.Len(cascadia.QueryAll(root, selector("#chat-view .dm-message")), 5)
|
||||
|
||||
// Should have the poller at the bottom
|
||||
node := cascadia.Query(root, selector("#new-messages-poller"))
|
||||
@ -782,7 +782,7 @@ func TestMessagesRoomPollForUpdates(t *testing.T) {
|
||||
resp := recorder.Result()
|
||||
root, err := html.Parse(resp.Body)
|
||||
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
|
||||
node := cascadia.Query(root, selector("#new-messages-poller"))
|
||||
@ -811,7 +811,7 @@ func TestMessagesRoomPollForUpdatesEmptyResult(t *testing.T) {
|
||||
resp := recorder.Result()
|
||||
root, err := html.Parse(resp.Body)
|
||||
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
|
||||
node := cascadia.Query(root, selector("#new-messages-poller"))
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,6 @@
|
||||
{{define "error-toast"}}
|
||||
<div class="server-error-msg">
|
||||
<div class="error-msg-container">
|
||||
<dialog class="server-error-msg" open>
|
||||
<span>{{.ErrorMsg}}</span>
|
||||
<button class="suicide" onclick="htmx.remove('.server-error-msg')">X</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{{end}}
|
||||
|
@ -1,25 +1,16 @@
|
||||
{{define "author-info"}}
|
||||
<div class="author-info" hx-boost="true">
|
||||
<a class="unstyled-link" href="/{{.Handle}}">
|
||||
<img
|
||||
class="profile-image"
|
||||
{{if .IsContentDownloaded}}
|
||||
src="/content/{{.GetProfileImageLocalPath}}"
|
||||
{{else}}
|
||||
src="{{.ProfileImageUrl}}"
|
||||
{{end}}
|
||||
/>
|
||||
</a>
|
||||
<span class="name-and-handle">
|
||||
<div class="display-name row">
|
||||
{{template "circle-profile-img" .}}
|
||||
<span class="author-info__name-and-handle">
|
||||
<div class="author-info__display-name row">
|
||||
{{.DisplayName}}
|
||||
{{if .IsPrivate}}
|
||||
<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>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="handle">@{{.Handle}}</div>
|
||||
<div class="author-info__handle">@{{.Handle}}</div>
|
||||
</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -24,11 +24,11 @@
|
||||
</head>
|
||||
<body>
|
||||
<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"/>
|
||||
</a>
|
||||
<form class="search-form" hx-get="/search" hx-push-url="true" hx-target="body" hx-swap="inner-html show:window:top">
|
||||
<input id="search-bar"
|
||||
<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" class="search-bar__input"
|
||||
name="q"
|
||||
placeholder="Search" type="text"
|
||||
{{with (search_text)}} value="{{.}}" {{end}}
|
||||
@ -39,9 +39,9 @@
|
||||
<main>
|
||||
{{template "main" .}}
|
||||
</main>
|
||||
<dialog id="image_carousel">
|
||||
<a class="quick-link close-button" onclick="image_carousel.close()">X</a>
|
||||
<img src="">
|
||||
<dialog id="image_carousel" class="image-carousel">
|
||||
<a class="button image-carousel__close-button" onclick="image_carousel.close()">X</a>
|
||||
<img class="image-carousel__active-image" src="">
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
||||
|
29
internal/webserver/tpl/includes/circle_profile_img.tpl
Normal file
29
internal/webserver/tpl/includes/circle_profile_img.tpl
Normal 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}}
|
@ -1,16 +1,16 @@
|
||||
{{define "likes-count"}}
|
||||
<div class="interaction-stat" hx-trigger="click consume">
|
||||
<div class="interactions__stat" hx-trigger="click consume">
|
||||
{{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-target="closest .interaction-stat"
|
||||
hx-target="closest .interactions__stat"
|
||||
hx-push-url="false"
|
||||
hx-swap="outerHTML focus-scroll:false"
|
||||
/>
|
||||
{{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-target="closest .interaction-stat"
|
||||
hx-target="closest .interactions__stat"
|
||||
hx-push-url="false"
|
||||
hx-swap="outerHTML focus-scroll:false"
|
||||
/>
|
||||
|
@ -1,74 +1,74 @@
|
||||
{{define "nav-sidebar"}}
|
||||
<nav id="nav-sidebar">
|
||||
<nav id="nav-sidebar" class="nav-sidebar">
|
||||
<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}}
|
||||
<img class="svg-icon" src="/static/icons/dotdotdot.svg" width="24" height="24" />
|
||||
</div>
|
||||
</div>
|
||||
<ul class="quick-links">
|
||||
<a class="unstyled-link" href="/timeline">
|
||||
<li class="quick-link">
|
||||
<ul class="nav-sidebar__buttons">
|
||||
<a href="/timeline">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
<a class="unstyled-link" onclick="document.querySelector('#search-bar').focus()">
|
||||
<li class="quick-link">
|
||||
<a onclick="document.querySelector('#search-bar').focus()">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
<a class="unstyled-link" href="#">
|
||||
<li class="quick-link">
|
||||
<a href="#">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
{{if (not (eq (active_user).Handle "[nobody]"))}}
|
||||
<a class="unstyled-link" href="/messages">
|
||||
<li class="quick-link">
|
||||
<a href="/messages">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="unstyled-link" href="/lists">
|
||||
<li class="quick-link">
|
||||
<a href="/lists">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
<a class="unstyled-link" href="#">
|
||||
<li class="quick-link">
|
||||
<a href="#">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
<a class="unstyled-link" href="#">
|
||||
<li class="quick-link">
|
||||
<a href="#">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
<a class="unstyled-link" href="#">
|
||||
<li class="quick-link">
|
||||
<a href="#">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
{{if (not (eq (active_user).Handle "[nobody]"))}}
|
||||
<a class="unstyled-link" href="/{{(active_user).Handle}}">
|
||||
<li class="quick-link">
|
||||
<a href="/{{(active_user).Handle}}">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
{{end}}
|
||||
<a class="unstyled-link" href="#">
|
||||
<li class="quick-link">
|
||||
<a href="#">
|
||||
<li class="button labelled-icon">
|
||||
<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>
|
||||
</a>
|
||||
</ul>
|
||||
|
@ -1,64 +1,68 @@
|
||||
{{define "user-header"}}
|
||||
<div class="user-header">
|
||||
{{if .BannerImageLocalPath}}
|
||||
<img class="user-header__profile-banner-image"
|
||||
{{if .IsContentDownloaded}}
|
||||
<img class="profile-banner-image" src="/content/profile_images/{{.BannerImageLocalPath}}" />
|
||||
src="/content/profile_images/{{.BannerImageLocalPath}}"
|
||||
{{else}}
|
||||
<img class="profile-banner-image" src="{{.BannerImageUrl}}" />
|
||||
src="{{.BannerImageUrl}}"
|
||||
{{end}}
|
||||
>
|
||||
{{end}}
|
||||
|
||||
<div class="user-header-info-container">
|
||||
<div class="user-header__info-container">
|
||||
<div class="row">
|
||||
{{template "author-info" .}}
|
||||
{{template "following-button" .}}
|
||||
</div>
|
||||
<div class="user-bio">
|
||||
<div class="user-header__bio">
|
||||
{{template "text-with-entities" .Bio}}
|
||||
</div>
|
||||
{{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" />
|
||||
<span>{{.Location}}</span>
|
||||
<label>{{.Location}}</label>
|
||||
</div>
|
||||
{{end}}
|
||||
{{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" />
|
||||
<a class="unstyled-link" target="_blank" href="{{.Website}}">{{.Website}}</a>
|
||||
<label><a target="_blank" href="{{.Website}}">{{.Website}}</a></label>
|
||||
</div>
|
||||
{{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" />
|
||||
<span>{{.JoinDate.Time.Format "Jan 2, 2006"}}</span>
|
||||
<label>{{.JoinDate.Time.Format "Jan 2, 2006"}}</label>
|
||||
</div>
|
||||
|
||||
<div class="followers-followees-container row">
|
||||
<a href="/{{.Handle}}/followers" class="followers-container unstyled-link">
|
||||
<span class="followers-count">{{.FollowersCount}}</span>
|
||||
<span class="followers-label">followers</span>
|
||||
<div class="followers-followees row">
|
||||
<a href="/{{.Handle}}/followers" class="followers-followees__followers">
|
||||
<span class="followers-followees__count">{{.FollowersCount}}</span>
|
||||
<label>followers</label>
|
||||
</a>
|
||||
<a href="/{{.Handle}}/followees" class="followers-container unstyled-link">
|
||||
<span class="following-label">is following</span>
|
||||
<span class="following-count">{{.FollowingCount}}</span>
|
||||
<a href="/{{.Handle}}/followees" class="followers-followees__followees">
|
||||
<label>is following</label>
|
||||
<span class="followers-followees__count">{{.FollowingCount}}</span>
|
||||
</a>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
||||
<div class="user-feed-buttons-container">
|
||||
<a class="unstyled-link quick-link" target="_blank" href="https://twitter.com/{{.Handle}}" title="Open on twitter.com">
|
||||
<div class="row">
|
||||
<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" />
|
||||
</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" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="htmx-spinner-container">
|
||||
<div class="htmx-spinner-background"></div>
|
||||
<img class="svg-icon htmx-spinner" src="/static/icons/spinner.svg" />
|
||||
<div class="htmx-spinner">
|
||||
<div class="htmx-spinner__fullscreen-forcer">
|
||||
<div class="htmx-spinner__background"></div>
|
||||
<img class="svg-icon htmx-spinner__icon" src="/static/icons/spinner.svg" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -4,12 +4,12 @@
|
||||
<div class="list-feed-header">
|
||||
<h1>{{.List.Name}}</h1>
|
||||
|
||||
<div class="row tabs-container">
|
||||
<a class="tab unstyled-link {{if (eq .ActiveTab "feed")}}active-tab{{end}}" href="/lists/{{.List.ID}}">
|
||||
<span class="tab-inner">Feed</span>
|
||||
<div class="tabs row">
|
||||
<a class="tabs__tab {{if (eq .ActiveTab "feed")}}tabs__tab--active{{end}}" href="/lists/{{.List.ID}}">
|
||||
<span class="tabs__tab-label">Feed</span>
|
||||
</a>
|
||||
<a class="tab unstyled-link {{if (eq .ActiveTab "users")}}active-tab{{end}}" href="/lists/{{.List.ID}}/users">
|
||||
<span class="tab-inner">Users</span>
|
||||
<a class="tabs__tab {{if (eq .ActiveTab "users")}}tabs__tab--active{{end}}" href="/lists/{{.List.ID}}/users">
|
||||
<span class="tabs__tab-label">Users</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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}}
|
@ -14,27 +14,18 @@
|
||||
<button onclick="document.querySelector('#newListDialog').close()">Cancel</button>
|
||||
</dialog>
|
||||
|
||||
<div class="users-list-previews">
|
||||
<div class="list-of-lists">
|
||||
{{range .}}
|
||||
{{$max_display_users := 10}}
|
||||
<div class="users-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 row row--spread">
|
||||
<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="num-users">({{(len .Users)}})</span>
|
||||
<div class="first-N-profile-images" hx-trigger="click consume">
|
||||
<span class="list-preview__num-users">({{(len .Users)}})</span>
|
||||
<div class="list-preview__first-N-profile-images" hx-trigger="click consume">
|
||||
{{range $i, $user := .Users}}
|
||||
{{/* Only render the first 10-ish users */}}
|
||||
{{if (lt $i $max_display_users)}}
|
||||
<a class="unstyled-link" href="/{{$user.Handle}}">
|
||||
<img
|
||||
class="profile-image"
|
||||
{{if $user.IsContentDownloaded}}
|
||||
src="/content/{{$user.GetProfileImageLocalPath}}"
|
||||
{{else}}
|
||||
src="{{$user.ProfileImageUrl}}"
|
||||
{{end}}
|
||||
/>
|
||||
</a>
|
||||
{{template "circle-profile-img" $user}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{if (gt (len .Users) $max_display_users)}}
|
||||
@ -42,7 +33,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<a class="unstyled-link quick-link danger"
|
||||
<a class="button button--danger"
|
||||
hx-delete="/lists/{{.ID}}" hx-target="body"
|
||||
onclick="return confirm('Delete this list? Are you sure?')"
|
||||
>Delete</a>
|
||||
|
@ -1,44 +1,49 @@
|
||||
{{define "title"}}Login{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<div class="login">
|
||||
<form hx-post="/change-session" hx-target="#nav-sidebar" hx-swap="outerHTML" hx-ext="json-enc">
|
||||
<label for="select-account">Choose account:</label>
|
||||
<select name="account" id="select-account">
|
||||
<div class="login-page">
|
||||
<h1>Login</h1>
|
||||
|
||||
<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}}
|
||||
<option value="{{.}}">@{{.}}</option>
|
||||
{{end}}
|
||||
<option value="no account">[no account (don't log in)]</option>
|
||||
</select>
|
||||
<div class="field-container submit-container">
|
||||
<input type='submit' value='Use account'>
|
||||
<div class="login-form__field-container login-form__submit-container">
|
||||
<input type='submit' value='Go'>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p>Or log in</p>
|
||||
<hr>
|
||||
|
||||
<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>
|
||||
{{with .FormErrors.username}}
|
||||
<label class='error'>({{.}})</label>
|
||||
<label class='login-form__error-label'>({{.}})</label>
|
||||
{{end}}
|
||||
<input name='username' value='{{.Username}}'>
|
||||
<input name='username' value='{{.Username}}' class="login-form__input">
|
||||
</div>
|
||||
<div class="field-container">
|
||||
<div class="login-form__field-container">
|
||||
<label>Password:</label>
|
||||
{{with .FormErrors.password}}
|
||||
<label class='error'>({{.}})</label>
|
||||
<label class='login-form__error-label'>({{.}})</label>
|
||||
{{end}}
|
||||
<input type='password' name='password'>
|
||||
<input type='password' name='password' class="login-form__input">
|
||||
</div>
|
||||
<div class="field-container submit-container">
|
||||
<div class="login-form__field-container login-form__submit-container">
|
||||
<input type='submit' value='Login'>
|
||||
</div>
|
||||
|
||||
<div class="htmx-spinner-container">
|
||||
<div class="htmx-spinner-background"></div>
|
||||
<img class="svg-icon htmx-spinner" src="/static/icons/spinner.svg" />
|
||||
<div class="htmx-spinner">
|
||||
<div class="htmx-spinner__background"></div>
|
||||
<img class="svg-icon htmx-spinner__icon" src="/static/icons/spinner.svg" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
{{define "title"}}Messages{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<div class="chats-container">
|
||||
<div class="messages-page">
|
||||
{{template "chat-list" .}}
|
||||
{{template "chat-view" .}}
|
||||
</div>
|
||||
|
@ -2,12 +2,12 @@
|
||||
|
||||
{{define "main"}}
|
||||
<div class="timeline-header">
|
||||
<div class="row tabs-container">
|
||||
<a class="tab unstyled-link {{if (eq .ActiveTab "User feed")}}active-tab{{end}}" href="/timeline">
|
||||
<span class="tab-inner">User feed</span>
|
||||
<div class="tabs row">
|
||||
<a class="tabs__tab {{if (eq .ActiveTab "User feed")}}tabs__tab--active{{end}}" href="/timeline">
|
||||
<span class="tabs__tab-label">User feed</span>
|
||||
</a>
|
||||
<a class="tab unstyled-link {{if (eq .ActiveTab "Offline")}}active-tab{{end}}" href="/timeline/offline">
|
||||
<span class="tab-inner">Offline timeline</span>
|
||||
<a class="tabs__tab {{if (eq .ActiveTab "Offline")}}tabs__tab--active{{end}}" href="/timeline/offline">
|
||||
<span class="tabs__tab-label">Offline timeline</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,35 +5,35 @@
|
||||
<div class="row row--spread">
|
||||
<div class="dummy"></div> {{/* Extra div to take up a slot in the `row` */}}
|
||||
<h1>Search results: {{.SearchText}}</h1>
|
||||
<div class="user-feed-buttons-container">
|
||||
<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">
|
||||
<div class="row">
|
||||
<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" />
|
||||
</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" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row tabs-container">
|
||||
<a class="tab unstyled-link {{if (not .IsUsersSearch)}}active-tab{{end}}" href="?type=tweets">
|
||||
<span class="tab-inner">Tweets</span>
|
||||
<div class="tabs row">
|
||||
<a class="tabs__tab {{if (not .IsUsersSearch)}}tabs__tab--active{{end}}" href="?type=tweets">
|
||||
<span class="tabs__tab-label">Tweets</span>
|
||||
</a>
|
||||
<a class="tab unstyled-link {{if .IsUsersSearch}}active-tab{{end}}" href="?type=users">
|
||||
<span class="tab-inner">Users</span>
|
||||
<a class="tabs__tab {{if .IsUsersSearch}}tabs__tab--active{{end}}" href="?type=users">
|
||||
<span class="tabs__tab-label">Users</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="htmx-spinner-container">
|
||||
<div class="htmx-spinner-background"></div>
|
||||
<img class="svg-icon htmx-spinner" src="/static/icons/spinner.svg" />
|
||||
<div class="htmx-spinner">
|
||||
<div class="htmx-spinner__background"></div>
|
||||
<img class="svg-icon htmx-spinner__icon" src="/static/icons/spinner.svg" />
|
||||
</div>
|
||||
</div>
|
||||
{{if .IsUsersSearch}}
|
||||
{{template "list" (dict "UserIDs" .UserIDs)}}
|
||||
{{else}}
|
||||
<div class="sort-order-container">
|
||||
<span class="sort-order-label">order:</span>
|
||||
<select name="sort-order" hx-get="#" hx-target="body" hx-push-url="true">
|
||||
<div class="sort-order">
|
||||
<label class="sort-order__label">order:</label>
|
||||
<select class="sort-order__dropdown" name="sort-order" hx-get="#" hx-target="body" hx-push-url="true">
|
||||
{{range .SortOrderOptions}}
|
||||
<option
|
||||
value="{{.}}"
|
||||
|
@ -2,12 +2,14 @@
|
||||
|
||||
|
||||
{{define "main"}}
|
||||
<div class="tweet-detail">
|
||||
{{range .ParentIDs}}
|
||||
<div class="thread-parent-tweet">
|
||||
{{template "tweet" (dict "TweetID" . "RetweetID" 0 "QuoteNestingLevel" 0)}}
|
||||
</div>
|
||||
{{end}}
|
||||
<div id="focused-tweet">
|
||||
|
||||
<div id="focused-tweet" class="focused-tweet">
|
||||
{{template "tweet" (dict "TweetID" .MainTweetID "RetweetID" 0 "QuoteNestingLevel" 0)}}
|
||||
</div>
|
||||
|
||||
@ -30,4 +32,5 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -7,7 +7,7 @@
|
||||
{{/* Scroll the active chat into view, if there is one */}}
|
||||
{{if $.ActiveRoomID}}
|
||||
<script>
|
||||
document.querySelector(".chat.active-chat").scrollIntoViewIfNeeded(true)
|
||||
document.querySelector(".chat-list-entry.chat-list-entry--active-chat").scrollIntoViewIfNeeded(true)
|
||||
</script>
|
||||
{{end}}
|
||||
</div>
|
||||
|
@ -1,7 +1,12 @@
|
||||
{{define "chat-list-entry"}}
|
||||
{{$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-preview-header">
|
||||
<div class="chat-list-entry {{if .is_active}}chat-list-entry--active-chat{{end}}"
|
||||
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")}}
|
||||
{{range $room.Participants}}
|
||||
{{if (ne .UserID (active_user).ID)}}
|
||||
@ -12,19 +17,21 @@
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{else}}
|
||||
<div class="groupchat-profile-image-container">
|
||||
<img class="profile-image" src="{{$room.AvatarImageRemoteURL}}" width="48" height="48" />
|
||||
<div class="chat-list-entry__groupchat-profile-image">
|
||||
{{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>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="chat-preview-timestamp .posted-at-container">
|
||||
<p class="posted-at">
|
||||
<div class="posted-at">
|
||||
<p class="posted-at__text">
|
||||
{{$room.LastMessagedAt.Time.Format "Jan 2, 2006"}}
|
||||
<br/>
|
||||
{{$room.LastMessagedAt.Time.Format "3:04 pm"}}
|
||||
</p>
|
||||
</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>
|
||||
{{end}}
|
||||
|
@ -1,25 +1,19 @@
|
||||
{{define "messages-with-poller"}}
|
||||
{{define "messages"}}
|
||||
{{range .MessageIDs}}
|
||||
{{$message := (index $.DMTrove.Messages .)}}
|
||||
{{$user := (user $message.SenderID)}}
|
||||
{{$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-container">
|
||||
<div class="sender-profile-image-container">
|
||||
<a class="unstyled-link" href="/{{$user.Handle}}">
|
||||
{{if $user.IsContentDownloaded}}
|
||||
<img class="profile-image" src="/content/{{$user.GetProfileImageLocalPath}}" />
|
||||
{{else}}
|
||||
<img class="profile-image" src="{{$user.ProfileImageUrl}}" />
|
||||
{{end}}
|
||||
</a>
|
||||
<div class="dm-message {{if $is_us}} our-message {{end}}">
|
||||
<div class="dm-message__row row">
|
||||
<div class="dm-message__sender-profile-img">
|
||||
{{template "circle-profile-img" $user}}
|
||||
</div>
|
||||
<div class="dm-message-content-container">
|
||||
<div class="dm-message__contents">
|
||||
{{if (ne $message.InReplyToID 0)}}
|
||||
<div class="replying-to-container">
|
||||
<div class="replying-to-label row">
|
||||
<div class="dm-message__replying-to">
|
||||
<div class="dm-message__replying-to-label labelled-icon">
|
||||
<img class="svg-icon" src="/static/icons/replying_to.svg" width="24" height="24" />
|
||||
<span>Replying to</span>
|
||||
<label>Replying to</label>
|
||||
</div>
|
||||
<div class="replying-to-message">
|
||||
{{(index $.DMTrove.Messages $message.InReplyToID).Text}}
|
||||
@ -27,7 +21,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{if (ne $message.EmbeddedTweetID 0)}}
|
||||
<div class="tweet-preview">
|
||||
<div class="dm-message__tweet-preview">
|
||||
{{template "tweet" (dict
|
||||
"TweetID" $message.EmbeddedTweetID
|
||||
"RetweetID" 0
|
||||
@ -36,7 +30,7 @@
|
||||
</div>
|
||||
{{end}}
|
||||
{{range $message.Images}}
|
||||
<img class="dm-embedded-image"
|
||||
<img class="dm-message__embedded-image"
|
||||
{{if .IsDownloaded}}
|
||||
src="/content/images/{{.LocalFilename}}"
|
||||
{{else}}
|
||||
@ -47,7 +41,7 @@
|
||||
>
|
||||
{{end}}
|
||||
{{range $message.Videos}}
|
||||
<video controls width="{{.Width}}" height="{{.Height}}"
|
||||
<video class="dm-message__embedded-video" controls width="{{.Width}}" height="{{.Height}}"
|
||||
{{if .IsDownloaded}}
|
||||
poster="/content/video_thumbnails/{{.ThumbnailLocalPath}}"
|
||||
{{else}}
|
||||
@ -62,47 +56,32 @@
|
||||
</video>
|
||||
{{end}}
|
||||
{{range $message.Urls}}
|
||||
<a
|
||||
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>
|
||||
{{template "embedded-link" .}}
|
||||
{{end}}
|
||||
{{if $message.Text}}
|
||||
<div class="dm-message-text-container">
|
||||
<div class="dm-message__text-content">
|
||||
{{template "text-with-entities" $message.Text}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dm-message-reactions">
|
||||
<div class="dm-message__reactions">
|
||||
{{range $message.Reactions}}
|
||||
{{$sender := (user .SenderID)}}
|
||||
<span title="{{$sender.DisplayName}} (@{{$sender.Handle}})">{{.Emoji}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
<p class="posted-at">
|
||||
<div class="sent-at">
|
||||
<p class="sent-at__text">
|
||||
{{$message.SentAt.Time.Format "Jan 2, 2006 @ 3:04 pm"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
{{define "messages-with-poller"}}
|
||||
{{template "messages" .}}
|
||||
|
||||
<div id="new-messages-poller"
|
||||
hx-swap="outerHTML {{if $.ScrollBottom}}scroll:.chat-messages:bottom{{end}}"
|
||||
@ -119,17 +98,21 @@
|
||||
{{end}}
|
||||
</div>
|
||||
{{if $.ActiveRoomID}}
|
||||
<div class="dm-composer-container">
|
||||
<form hx-post="/messages/{{$.ActiveRoomID}}/send" hx-target="#new-messages-poller" hx-swap="outerHTML scroll:.chat-messages:bottom" hx-ext="json-enc">
|
||||
<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"
|
||||
>
|
||||
{{template "dm-composer"}}
|
||||
<input id="real-input" type="hidden" name="text" value="" />
|
||||
<input id="realInput" type="hidden" name="text" value="" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
// Make pasting text work for HTML as well as plain text
|
||||
var editor = document.querySelector("#composer");
|
||||
editor.addEventListener("paste", function(e) {
|
||||
composer.addEventListener("paste", function(e) {
|
||||
// cancel paste
|
||||
e.preventDefault();
|
||||
// get text representation of clipboard
|
||||
@ -180,10 +163,7 @@
|
||||
{{end}}
|
||||
|
||||
{{define "dm-composer"}}
|
||||
<span
|
||||
id="composer"
|
||||
role="textbox"
|
||||
contenteditable
|
||||
<span id="composer" role="textbox" contenteditable oninput="realInput.value = this.innerText"
|
||||
{{if .}}
|
||||
{{/*
|
||||
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"
|
||||
{{end}}
|
||||
oninput="var text = this.innerText; document.querySelector('#real-input').value = text"
|
||||
>
|
||||
</span>
|
||||
></span>
|
||||
{{end}}
|
||||
|
24
internal/webserver/tpl/tweet_page_includes/embedded_link.tpl
Normal file
24
internal/webserver/tpl/tweet_page_includes/embedded_link.tpl
Normal 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}}
|
@ -1,12 +1,16 @@
|
||||
{{define "list"}}
|
||||
<div class="users-list-container">
|
||||
<div class="users-list">
|
||||
{{range .UserIDs}}
|
||||
{{$user := (user .)}}
|
||||
<div class="user">
|
||||
<div class="row row--spread">
|
||||
{{template "author-info" $user}}
|
||||
{{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}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
@ -1,9 +1,9 @@
|
||||
{{define "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-info-container row">
|
||||
<span class="poll-choice-label">{{.label}}</span>
|
||||
<span class="poll-choice-votes">{{.votes}} ({{printf "%.1f" (.poll.VotePercentage .votes)}}%)</span>
|
||||
<div class="row poll__choice">
|
||||
<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__choice-info row">
|
||||
<span class="poll__choice-label">{{.label}}</span>
|
||||
<span class="poll__choice-votes">{{.votes}} ({{printf "%.1f" (.poll.VotePercentage .votes)}}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -21,8 +21,8 @@
|
||||
{{template "poll-choice" (dict "label" .Choice4 "votes" .Choice4_Votes "poll" .)}}
|
||||
{{end}}
|
||||
|
||||
<p class="poll-metadata">
|
||||
<span class="poll-state">
|
||||
<p class="poll__metadata">
|
||||
<span class="poll__metadata__state">
|
||||
{{if .IsOpen}}
|
||||
Poll open, voting ends at {{.FormatEndsAt}}
|
||||
{{else}}
|
||||
|
@ -13,30 +13,36 @@
|
||||
{{if (not (eq .RetweetID 0))}}
|
||||
{{$retweet := (retweet .RetweetID)}}
|
||||
{{$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" />
|
||||
<span class="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">
|
||||
<span class="retweet-info__retweeted-by-label">Retweeted by</span>
|
||||
<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}}
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="tweet-header-container">
|
||||
<div class="tweet__header-container">
|
||||
<div class="author-info-container" hx-trigger="click consume">
|
||||
{{template "author-info" $author}}
|
||||
</div>
|
||||
{{if $main_tweet.ReplyMentions}}
|
||||
<div class="reply-mentions-container" hx-trigger="click consume">
|
||||
<span class="replying-to-label">Replying to</span>
|
||||
<ul class="reply-mentions inline-dotted-list">
|
||||
<div class="reply-mentions" hx-trigger="click consume">
|
||||
<span class="reply-mentions__dm-message__replying-to-label">Replying to</span>
|
||||
<ul class="reply-mentions__list inline-dotted-list">
|
||||
{{range $main_tweet.ReplyMentions}}
|
||||
<li><a class="entity" href="/{{.}}">@{{.}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="posted-at-container">
|
||||
<p class="posted-at">
|
||||
<div class="posted-at">
|
||||
<p class="posted-at__text">
|
||||
{{$main_tweet.PostedAt.Time.Format "Jan 2, 2006"}}
|
||||
<br/>
|
||||
{{$main_tweet.PostedAt.Time.Format "3:04 pm"}}
|
||||
@ -44,11 +50,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="vertical-reply-line-container">
|
||||
<div class="vertical-reply-line">
|
||||
<span class="string-box">
|
||||
<div class="string">
|
||||
</div>
|
||||
</span>
|
||||
<span class="vertical-container-1">
|
||||
<span class="tweet__vertical-container">
|
||||
<div class="tweet-content">
|
||||
{{if (ne $main_tweet.TombstoneType "")}}
|
||||
<div class="tombstone">
|
||||
@ -57,7 +63,7 @@
|
||||
{{end}}
|
||||
{{template "text-with-entities" $main_tweet.Text}}
|
||||
{{range $main_tweet.Images}}
|
||||
<img class="tweet-image"
|
||||
<img class="tweet__embedded-image"
|
||||
{{if .IsDownloaded}}
|
||||
src="/content/images/{{.LocalFilename}}"
|
||||
{{else}}
|
||||
@ -69,7 +75,7 @@
|
||||
{{end}}
|
||||
hx-trigger="click consume"
|
||||
onclick="image_carousel.querySelector('img').src = this.src; image_carousel.showModal();"
|
||||
/>
|
||||
>
|
||||
{{end}}
|
||||
{{range $main_tweet.Videos}}
|
||||
<video controls hx-trigger="click consume" width="{{.Width}}" height="{{.Height}}"
|
||||
@ -88,28 +94,7 @@
|
||||
{{end}}
|
||||
{{range $main_tweet.Urls}}
|
||||
<div class="click-eater" hx-trigger="click consume">
|
||||
<a
|
||||
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>
|
||||
{{template "embedded-link" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{range $main_tweet.Polls}}
|
||||
@ -117,8 +102,12 @@
|
||||
{{end}}
|
||||
|
||||
{{if (and $main_tweet.QuotedTweetID (lt .QuoteNestingLevel 1))}}
|
||||
<div class="quoted-tweet rounded-gray-outline" hx-trigger="click consume">
|
||||
{{template "tweet" (dict "TweetID" $main_tweet.QuotedTweetID "RetweetID" 0 "QuoteNestingLevel" (add .QuoteNestingLevel 1))}}
|
||||
<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)
|
||||
) }}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if $main_tweet.SpaceID}}
|
||||
@ -126,35 +115,46 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="interactions-bar row">
|
||||
<div class="interaction-stat">
|
||||
<div class="interactions row">
|
||||
<div class="interactions__stat">
|
||||
<img class="svg-icon" src="/static/icons/quote.svg" width="24" height="24" />
|
||||
<span>{{$main_tweet.NumQuoteTweets}}</span>
|
||||
</div>
|
||||
<div class="interaction-stat">
|
||||
<div class="interactions__stat">
|
||||
<img class="svg-icon" src="/static/icons/reply.svg" width="24" height="24" />
|
||||
<span>{{$main_tweet.NumReplies}}</span>
|
||||
</div>
|
||||
<div class="interaction-stat">
|
||||
<div class="interactions__stat">
|
||||
<img class="svg-icon" src="/static/icons/retweet.svg" width="24" height="24" />
|
||||
<span>{{$main_tweet.NumRetweets}}</span>
|
||||
</div>
|
||||
{{template "likes-count" $main_tweet}}
|
||||
<div class="dummy"></div>
|
||||
<div class="tweet-buttons-container" 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">
|
||||
<div class="interactions__dummy"></div>
|
||||
<div class="row" hx-trigger="click consume">
|
||||
<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" />
|
||||
</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" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<div class="htmx-spinner-container">
|
||||
<div class="htmx-spinner-background"></div>
|
||||
<img class="svg-icon htmx-spinner" src="/static/icons/spinner.svg" />
|
||||
<div class="htmx-spinner">
|
||||
<div class="htmx-spinner__background"></div>
|
||||
<img class="svg-icon htmx-spinner__icon" src="/static/icons/spinner.svg" />
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
@ -1,18 +1,18 @@
|
||||
{{define "space"}}
|
||||
<div class="space">
|
||||
<div class="space-host row">
|
||||
<div class="space__host row">
|
||||
{{template "author-info" (user .CreatedById)}}
|
||||
<span class="host-label">(Host)</span>
|
||||
<div class="layout-spacer"></div>
|
||||
<div class="space-date">
|
||||
<span class="space__host__label">(Host)</span>
|
||||
<div class="space__layout-spacer"></div>
|
||||
<div class="space__date">
|
||||
{{.StartedAt.Format "Jan 2, 2006"}}<br>{{.StartedAt.Format "3:04pm"}}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="space-title">{{.Title}}</h3>
|
||||
<div class="space-info row">
|
||||
<h3 class="space__title">{{.Title}}</h3>
|
||||
<div class="space__info row">
|
||||
<span class="space-state">
|
||||
{{if (eq .State "Ended")}}
|
||||
<ul class="space-info-list inline-dotted-list">
|
||||
<ul class="space__info__list inline-dotted-list">
|
||||
<li>{{.State}}</li>
|
||||
<li>{{(len .ParticipantIds)}} participants</li>
|
||||
<li>{{.LiveListenersCount}} tuned in</li>
|
||||
@ -23,7 +23,7 @@
|
||||
{{end}}
|
||||
</span>
|
||||
</div>
|
||||
<ul class="space-participants-list">
|
||||
<ul class="space__participants-list">
|
||||
{{range .ParticipantIds}}
|
||||
{{if (ne . $.CreatedById)}}
|
||||
<li>{{template "author-info" (user .)}}</li>
|
||||
|
@ -3,5 +3,15 @@
|
||||
{{template "tweet" .}}
|
||||
{{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}}
|
||||
|
@ -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}}
|
@ -5,18 +5,18 @@
|
||||
<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>
|
||||
<div class="tabs row">
|
||||
<a class="tabs__tab {{if (eq .FeedType "")}}tabs__tab--active{{end}}" href="/{{$user.Handle}}">
|
||||
<span class="tabs__tab-label">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 class="tabs__tab {{if (eq .FeedType "without_replies")}}tabs__tab--active{{end}}" href="/{{$user.Handle}}/without_replies">
|
||||
<span class="tabs__tab-label">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 class="tabs__tab {{if (eq .FeedType "media")}}tabs__tab--active{{end}}" href="/{{$user.Handle}}/media">
|
||||
<span class="tabs__tab-label">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 class="tabs__tab {{if (eq .FeedType "likes")}}tabs__tab--active{{end}}" href="/{{$user.Handle}}/likes">
|
||||
<span class="tabs__tab-label">Likes</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -24,9 +24,9 @@
|
||||
<div class="timeline user-feed-timeline">
|
||||
{{if .PinnedTweet.ID}}
|
||||
<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" />
|
||||
<span>Pinned</span>
|
||||
<label>Pinned</label>
|
||||
</div>
|
||||
{{template "tweet" (dict "TweetID" .PinnedTweet.ID "RetweetID" 0 "QuoteNestingLevel" 0)}}
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user