mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 18:32:25 -05:00 
			
		
		
		
	[feature] Add back/next buttons to profiles for paging through statuses (#708)
* add GetAccountWebStatuses to db * add WebStatusesGet func to processor * don't add limit to next/prev links if 0 * take query params for next/prev statuses * add separate next + prev links for convenience * show 'nothing here' message if no statuses exist * add back / next links to profiles * allow paging down only * go fmt ./... * 'recent public toots' -> 'latest public toots'
This commit is contained in:
		
					parent
					
						
							
								6934ae378a
							
						
					
				
			
			
				commit
				
					
						6418307c64
					
				
			
		
					 11 changed files with 183 additions and 36 deletions
				
			
		|  | @ -25,4 +25,6 @@ import "github.com/superseriousbusiness/gotosocial/internal/timeline" | ||||||
| type TimelineResponse struct { | type TimelineResponse struct { | ||||||
| 	Items      []timeline.Timelineable | 	Items      []timeline.Timelineable | ||||||
| 	LinkHeader string | 	LinkHeader string | ||||||
|  | 	NextLink   string | ||||||
|  | 	PrevLink   string | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -54,6 +54,11 @@ type Account interface { | ||||||
| 	// In case of no entries, a 'no entries' error will be returned | 	// In case of no entries, a 'no entries' error will be returned | ||||||
| 	GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, Error) | 	GetAccountStatuses(ctx context.Context, accountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) ([]*gtsmodel.Status, Error) | ||||||
| 
 | 
 | ||||||
|  | 	// GetAccountWebStatuses is similar to GetAccountStatuses, but it's specifically for returning statuses that | ||||||
|  | 	// should be visible via the web view of an account. So, only public, federated statuses that aren't boosts | ||||||
|  | 	// or replies. | ||||||
|  | 	GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, Error) | ||||||
|  | 
 | ||||||
| 	GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error) | 	GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, Error) | ||||||
| 
 | 
 | ||||||
| 	// GetAccountLastPosted simply gets the timestamp of the most recent post by the account. | 	// GetAccountLastPosted simply gets the timestamp of the most recent post by the account. | ||||||
|  |  | ||||||
|  | @ -301,27 +301,33 @@ func (a *accountDB) GetAccountStatuses(ctx context.Context, accountID string, li | ||||||
| 		return nil, a.conn.ProcessError(err) | 		return nil, a.conn.ProcessError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Catch case of no statuses early | 	return a.statusesFromIDs(ctx, statusIDs) | ||||||
| 	if len(statusIDs) == 0 { |  | ||||||
| 		return nil, db.ErrNoEntries |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 	// Allocate return slice (will be at most len statusIDS) | func (a *accountDB) GetAccountWebStatuses(ctx context.Context, accountID string, limit int, maxID string) ([]*gtsmodel.Status, db.Error) { | ||||||
| 	statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) | 	statusIDs := []string{} | ||||||
| 
 | 
 | ||||||
| 	for _, id := range statusIDs { | 	q := a.conn. | ||||||
| 		// Fetch from status from database by ID | 		NewSelect(). | ||||||
| 		status, err := a.status.GetStatusByID(ctx, id) | 		Table("statuses"). | ||||||
| 		if err != nil { | 		Column("id"). | ||||||
| 			logrus.Errorf("GetAccountStatuses: error getting status %q: %v", id, err) | 		Where("account_id = ?", accountID). | ||||||
| 			continue | 		WhereGroup(" AND ", whereEmptyOrNull("in_reply_to_id")). | ||||||
|  | 		WhereGroup(" AND ", whereEmptyOrNull("boost_of_id")). | ||||||
|  | 		Where("visibility = ?", gtsmodel.VisibilityPublic). | ||||||
|  | 		Where("federated = ?", true) | ||||||
|  | 
 | ||||||
|  | 	if maxID != "" { | ||||||
|  | 		q = q.Where("id < ?", maxID) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 		// Append to return slice | 	q = q.Limit(limit).Order("id DESC") | ||||||
| 		statuses = append(statuses, status) | 
 | ||||||
|  | 	if err := q.Scan(ctx, &statusIDs); err != nil { | ||||||
|  | 		return nil, a.conn.ProcessError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return statuses, nil | 	return a.statusesFromIDs(ctx, statusIDs) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) { | func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxID string, sinceID string, limit int) ([]*gtsmodel.Account, string, string, db.Error) { | ||||||
|  | @ -363,3 +369,27 @@ func (a *accountDB) GetAccountBlocks(ctx context.Context, accountID string, maxI | ||||||
| 	prevMinID := blocks[0].ID | 	prevMinID := blocks[0].ID | ||||||
| 	return accounts, nextMaxID, prevMinID, nil | 	return accounts, nextMaxID, prevMinID, nil | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (a *accountDB) statusesFromIDs(ctx context.Context, statusIDs []string) ([]*gtsmodel.Status, db.Error) { | ||||||
|  | 	// Catch case of no statuses early | ||||||
|  | 	if len(statusIDs) == 0 { | ||||||
|  | 		return nil, db.ErrNoEntries | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Allocate return slice (will be at most len statusIDS) | ||||||
|  | 	statuses := make([]*gtsmodel.Status, 0, len(statusIDs)) | ||||||
|  | 
 | ||||||
|  | 	for _, id := range statusIDs { | ||||||
|  | 		// Fetch from status from database by ID | ||||||
|  | 		status, err := a.status.GetStatusByID(ctx, id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			logrus.Errorf("statusesFromIDs: error getting status %q: %v", id, err) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// Append to return slice | ||||||
|  | 		statuses = append(statuses, status) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return statuses, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -50,6 +50,10 @@ func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, | ||||||
| 	return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) | 	return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (p *processor) AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.TimelineResponse, gtserror.WithCode) { | ||||||
|  | 	return p.accountProcessor.WebStatusesGet(ctx, targetAccountID, maxID) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { | func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { | ||||||
| 	return p.accountProcessor.FollowersGet(ctx, authed.Account, targetAccountID) | 	return p.accountProcessor.FollowersGet(ctx, authed.Account, targetAccountID) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -56,6 +56,9 @@ type Processor interface { | ||||||
| 	// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for | 	// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for | ||||||
| 	// the account given in authed. | 	// the account given in authed. | ||||||
| 	StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) | 	StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) | ||||||
|  | 	// WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only | ||||||
|  | 	// statuses which are suitable for showing on the public web profile of an account. | ||||||
|  | 	WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.TimelineResponse, gtserror.WithCode) | ||||||
| 	// FollowersGet fetches a list of the target account's followers. | 	// FollowersGet fetches a list of the target account's followers. | ||||||
| 	FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) | 	FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) | ||||||
| 	// FollowingGet fetches a list of the accounts that target account is following. | 	// FollowingGet fetches a list of the accounts that target account is following. | ||||||
|  |  | ||||||
|  | @ -84,3 +84,45 @@ func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func (p *processor) WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.TimelineResponse, gtserror.WithCode) { | ||||||
|  | 	acct, err := p.db.GetAccountByID(ctx, targetAccountID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if err == db.ErrNoEntries { | ||||||
|  | 			err := fmt.Errorf("account %s not found in the db, not getting web statuses for it", targetAccountID) | ||||||
|  | 			return nil, gtserror.NewErrorNotFound(err) | ||||||
|  | 		} | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if acct.Domain != "" { | ||||||
|  | 		err := fmt.Errorf("account %s was not a local account, not getting web statuses for it", targetAccountID) | ||||||
|  | 		return nil, gtserror.NewErrorNotFound(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	statuses, err := p.db.GetAccountWebStatuses(ctx, targetAccountID, 10, maxID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if err == db.ErrNoEntries { | ||||||
|  | 			return util.EmptyTimelineResponse(), nil | ||||||
|  | 		} | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	timelineables := []timeline.Timelineable{} | ||||||
|  | 	for _, i := range statuses { | ||||||
|  | 		apiStatus, err := p.tc.StatusToAPIStatus(ctx, i, nil) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		timelineables = append(timelineables, apiStatus) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return util.PackageTimelineableResponse(util.TimelineableResponseParams{ | ||||||
|  | 		Items:            timelineables, | ||||||
|  | 		Path:             "/@" + acct.Username, | ||||||
|  | 		NextMaxIDValue:   timelineables[len(timelineables)-1].GetID(), | ||||||
|  | 		PrevMinIDValue:   timelineables[0].GetID(), | ||||||
|  | 		ExtraQueryParams: []string{}, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -84,6 +84,9 @@ type Processor interface { | ||||||
| 	// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for | 	// AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for | ||||||
| 	// the account given in authed. | 	// the account given in authed. | ||||||
| 	AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) | 	AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.TimelineResponse, gtserror.WithCode) | ||||||
|  | 	// AccountWebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only | ||||||
|  | 	// statuses which are suitable for showing on the public web profile of an account. | ||||||
|  | 	AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.TimelineResponse, gtserror.WithCode) | ||||||
| 	// AccountFollowersGet fetches a list of the target account's followers. | 	// AccountFollowersGet fetches a list of the target account's followers. | ||||||
| 	AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) | 	AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) | ||||||
| 	// AccountFollowingGet fetches a list of the accounts that target account is following. | 	// AccountFollowingGet fetches a list of the accounts that target account is following. | ||||||
|  |  | ||||||
|  | @ -61,12 +61,15 @@ func PackageTimelineableResponse(params TimelineableResponseParams) (*apimodel.T | ||||||
| 		Items: params.Items, | 		Items: params.Items, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// prepare the next and previous links |  | ||||||
| 	if len(params.Items) != 0 { | 	if len(params.Items) != 0 { | ||||||
| 		protocol := config.GetProtocol() | 		protocol := config.GetProtocol() | ||||||
| 		host := config.GetHost() | 		host := config.GetHost() | ||||||
| 
 | 
 | ||||||
| 		nextRaw := fmt.Sprintf("limit=%d&%s=%s", params.Limit, params.NextMaxIDKey, params.NextMaxIDValue) | 		// next | ||||||
|  | 		nextRaw := params.NextMaxIDKey + "=" + params.NextMaxIDValue | ||||||
|  | 		if params.Limit != 0 { | ||||||
|  | 			nextRaw = fmt.Sprintf("limit=%d&", params.Limit) + nextRaw | ||||||
|  | 		} | ||||||
| 		for _, p := range params.ExtraQueryParams { | 		for _, p := range params.ExtraQueryParams { | ||||||
| 			nextRaw = nextRaw + "&" + p | 			nextRaw = nextRaw + "&" + p | ||||||
| 		} | 		} | ||||||
|  | @ -76,9 +79,14 @@ func PackageTimelineableResponse(params TimelineableResponseParams) (*apimodel.T | ||||||
| 			Path:     params.Path, | 			Path:     params.Path, | ||||||
| 			RawQuery: nextRaw, | 			RawQuery: nextRaw, | ||||||
| 		} | 		} | ||||||
| 		next := fmt.Sprintf("<%s>; rel=\"next\"", nextLink.String()) | 		nextLinkString := nextLink.String() | ||||||
|  | 		timelineResponse.NextLink = nextLinkString | ||||||
| 
 | 
 | ||||||
| 		prevRaw := fmt.Sprintf("limit=%d&%s=%s", params.Limit, params.PrevMinIDKey, params.PrevMinIDValue) | 		// prev | ||||||
|  | 		prevRaw := params.PrevMinIDKey + "=" + params.PrevMinIDValue | ||||||
|  | 		if params.Limit != 0 { | ||||||
|  | 			prevRaw = fmt.Sprintf("limit=%d&", params.Limit) + prevRaw | ||||||
|  | 		} | ||||||
| 		for _, p := range params.ExtraQueryParams { | 		for _, p := range params.ExtraQueryParams { | ||||||
| 			prevRaw = prevRaw + "&" + p | 			prevRaw = prevRaw + "&" + p | ||||||
| 		} | 		} | ||||||
|  | @ -88,7 +96,12 @@ func PackageTimelineableResponse(params TimelineableResponseParams) (*apimodel.T | ||||||
| 			Path:     params.Path, | 			Path:     params.Path, | ||||||
| 			RawQuery: prevRaw, | 			RawQuery: prevRaw, | ||||||
| 		} | 		} | ||||||
| 		prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLink.String()) | 		prevLinkString := prevLink.String() | ||||||
|  | 		timelineResponse.PrevLink = prevLinkString | ||||||
|  | 
 | ||||||
|  | 		// link header | ||||||
|  | 		next := fmt.Sprintf("<%s>; rel=\"next\"", nextLinkString) | ||||||
|  | 		prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLinkString) | ||||||
| 		timelineResponse.LinkHeader = next + ", " + prev | 		timelineResponse.LinkHeader = next + ", " + prev | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -36,6 +36,11 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | const ( | ||||||
|  | 	// MaxStatusIDKey is for specifying the maximum ID of the status to retrieve. | ||||||
|  | 	MaxStatusIDKey = "max_id" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| func (m *Module) profileGETHandler(c *gin.Context) { | func (m *Module) profileGETHandler(c *gin.Context) { | ||||||
| 	ctx := c.Request.Context() | 	ctx := c.Request.Context() | ||||||
| 
 | 
 | ||||||
|  | @ -78,10 +83,18 @@ func (m *Module) profileGETHandler(c *gin.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// get latest 10 top-level public statuses; | 	// we should only show the 'back to top' button if the | ||||||
| 	// ie., exclude replies and boosts, public only, | 	// profile visitor is paging through statuses | ||||||
| 	// with or without media | 	showBackToTop := false | ||||||
| 	statusResp, errWithCode := m.processor.AccountStatusesGet(ctx, authed, account.ID, 10, true, true, "", "", false, false, true) | 
 | ||||||
|  | 	maxStatusID := "" | ||||||
|  | 	maxStatusIDString := c.Query(MaxStatusIDKey) | ||||||
|  | 	if maxStatusIDString != "" { | ||||||
|  | 		maxStatusID = maxStatusIDString | ||||||
|  | 		showBackToTop = true | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	statusResp, errWithCode := m.processor.AccountWebStatusesGet(ctx, account.ID, maxStatusID) | ||||||
| 	if errWithCode != nil { | 	if errWithCode != nil { | ||||||
| 		api.ErrorHandler(c, errWithCode, instanceGet) | 		api.ErrorHandler(c, errWithCode, instanceGet) | ||||||
| 		return | 		return | ||||||
|  | @ -106,6 +119,8 @@ func (m *Module) profileGETHandler(c *gin.Context) { | ||||||
| 		"instance":         instance, | 		"instance":         instance, | ||||||
| 		"account":          account, | 		"account":          account, | ||||||
| 		"statuses":         statusResp.Items, | 		"statuses":         statusResp.Items, | ||||||
|  | 		"statuses_next":    statusResp.NextLink, | ||||||
|  | 		"show_back_to_top": showBackToTop, | ||||||
| 		"stylesheets": []string{ | 		"stylesheets": []string{ | ||||||
| 			"/assets/Fork-Awesome/css/fork-awesome.min.css", | 			"/assets/Fork-Awesome/css/fork-awesome.min.css", | ||||||
| 			"/assets/dist/status.css", | 			"/assets/dist/status.css", | ||||||
|  |  | ||||||
|  | @ -160,6 +160,24 @@ main { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .nothinghere { | ||||||
|  | 	margin-left: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .backnextlinks { | ||||||
|  | 	display: flex; | ||||||
|  | 	flex-wrap: wrap; | ||||||
|  | 	justify-content: space-between; | ||||||
|  | 
 | ||||||
|  | 	a { | ||||||
|  | 		padding: 1rem; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	.next { | ||||||
|  | 		margin-left: auto; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .toot, .toot:last-child { | .toot, .toot:last-child { | ||||||
| 	box-shadow: $boxshadow; | 	box-shadow: $boxshadow; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -27,7 +27,10 @@ | ||||||
|             <div class="entry">Posted <b>{{.account.StatusesCount}}</b></div> |             <div class="entry">Posted <b>{{.account.StatusesCount}}</b></div> | ||||||
|         </div> |         </div> | ||||||
|     </div> |     </div> | ||||||
|     <h2 id="recent">Recent public toots</h2> |     <h2 id="recent">Latest public toots</h2> | ||||||
|  | 	    {{ if not .statuses }} | ||||||
|  |         <div class="nothinghere">Nothing here!</div> | ||||||
|  |         {{ else }} | ||||||
|         <div class="thread"> |         <div class="thread"> | ||||||
|             {{ range .statuses }} |             {{ range .statuses }} | ||||||
|             <div class="toot expanded"> |             <div class="toot expanded"> | ||||||
|  | @ -35,5 +38,14 @@ | ||||||
|             </div> |             </div> | ||||||
|             {{ end }} |             {{ end }} | ||||||
|         </div> |         </div> | ||||||
|  |         {{ end }} | ||||||
|  |     <div class="backnextlinks"> | ||||||
|  |         {{ if .show_back_to_top }} | ||||||
|  |         <a href="/@{{ .account.Username }}">Back to top</a> | ||||||
|  |         {{ end }} | ||||||
|  |         {{ if .statuses_next }} | ||||||
|  |         <a href="{{ .statuses_next }}" class="next">Show older</a> | ||||||
|  |         {{ end }} | ||||||
|  |     </div> | ||||||
| </main> | </main> | ||||||
| {{ template "footer.tmpl" .}} | {{ template "footer.tmpl" .}} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue