mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-28 03:50:10 +00:00
Compare commits
2 Commits
a0db685af2
...
47ce8a9ec4
Author | SHA1 | Date | |
---|---|---|---|
47ce8a9ec4 | |||
2d83718f81 |
@ -104,7 +104,7 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
||||
query = badgerhold.Where("Type").In(activityTypes...)
|
||||
}
|
||||
|
||||
if req.Ascending {
|
||||
if !req.Ascending {
|
||||
query = query.Reverse()
|
||||
}
|
||||
|
||||
@ -125,6 +125,7 @@ func (app *appContext) GetActivities(gc *gin.Context) {
|
||||
|
||||
resp := GetActivitiesRespDTO{
|
||||
Activities: make([]ActivityDTO, len(results)),
|
||||
LastPage: len(results) != req.Limit,
|
||||
}
|
||||
|
||||
for i, act := range results {
|
||||
|
@ -3,6 +3,10 @@
|
||||
color: rgba(0, 0, 0, 0) !important;
|
||||
}
|
||||
|
||||
.loader.rel {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loader .dot {
|
||||
--diameter: 0.5rem;
|
||||
--radius: calc(var(--diameter) / 2);
|
||||
@ -15,6 +19,12 @@
|
||||
left: calc(50% - var(--radius));
|
||||
animation: osc 1s cubic-bezier(.72,.16,.31,.97) infinite;
|
||||
}
|
||||
|
||||
.loader.rel .dot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
.loader.loader-sm .dot {
|
||||
--deviation: 10%;
|
||||
}
|
||||
|
@ -732,34 +732,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="button ~neutral @low ml-2" id="activity-sort-direction">{{ .strings.sortDirection }}</button>
|
||||
<input type="search" class="field ~neutral @low input search ml-2 mr-2" id="activity-search" placeholder="{{ .strings.search }}">
|
||||
<span class="button ~neutral @low center ml-[-2.64rem] rounded-s-none activity-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></span>
|
||||
<button class="button ~info @low ml-2" id="activity-refresh" aria-label="{{ .strings.refresh }}" disabled><i class="ri-refresh-line"></i></button>
|
||||
</div>
|
||||
<div class="supra py-1 sm hidden" id="activity-search-options-header">{{ .strings.searchOptions }}</div>
|
||||
<div class="row -mx-2 mb-2">
|
||||
<button type="button" class="button ~neutral @low center mx-2 hidden"><span id="activity-sort-by-field"></span> <i class="ri-close-line ml-2 text-2xl"></i></button>
|
||||
<span id="activity-filter-area"></span>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row gap-3">
|
||||
<div class="card @low dark:~d_neutral col max-w-[20%]">
|
||||
<div class="flex-expand">
|
||||
<input type="search" class="field ~neutral @low input settings-section-button justify-between mb-2" id="settings-search" placeholder="{{ .strings.search }}">
|
||||
<button class="button ~neutral @low center -ml-10 rounded-s-none mb-2 settings-search-clear" aria-label="{{ .strings.clearSearch }}" text="{{ .strings.clearSearch }}"><i class="ri-close-line"></i></button>
|
||||
<div class="my-2">
|
||||
<div id="activity-card-list"></div>
|
||||
<div id="activity-loader"></div>
|
||||
<div class="unfocused h-[100%] my-3" id="activity-not-found">
|
||||
<div class="flex flex-col h-[100%] justify-center items-center">
|
||||
<span class="text-2xl font-medium italic mb-3">{{ .strings.noResultsFound }}</span>
|
||||
<button class="button ~neutral @low activity-search-clear">
|
||||
<span class="mr-2">{{ .strings.clearSearch }}</span><i class="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
<aside class="aside sm ~urge dark:~d_info mb-2 @low" id="settings-message">Note: <span class="badge ~critical">*</span> indicates a required field, <span class="badge ~info dark:~d_warning">R</span> indicates changes require a restart.</aside>
|
||||
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-about"><span class="flex">{{ .strings.aboutProgram }} <i class="ri-information-line ml-2"></i></span></span>
|
||||
<span class="button ~neutral @low settings-section-button justify-between mb-2" id="setting-profiles"><span class="flex">{{ .strings.userProfiles }} <i class="ri-user-line ml-2"></i></span></span>
|
||||
</div>
|
||||
<div class="card ~neutral @low col">
|
||||
<div id="activity-card-list"></div>
|
||||
<div class="unfocused h-[100%] my-3" id="activity-not-found">
|
||||
<div class="flex flex-col h-[100%] justify-center items-center">
|
||||
<span class="text-2xl font-medium italic mb-3">{{ .strings.noResultsFound }}</span>
|
||||
<button class="button ~neutral @low activity-search-clear">
|
||||
<span class="mr-2">{{ .strings.clearSearch }}</span><i class="ri-close-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<button class="button my-2 ~neutral @low" id="activity-load-more">{{ .strings.loadMore }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -121,6 +121,7 @@
|
||||
"accessJFA": "Access jfa-go",
|
||||
"accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General.",
|
||||
"sortingBy": "Sorting By",
|
||||
"sortDirection": "Sort Direction",
|
||||
"filters": "Filters",
|
||||
"clickToRemoveFilter": "Click to remove this filter.",
|
||||
"clearSearch": "Clear search",
|
||||
@ -156,7 +157,7 @@
|
||||
"title": "Title",
|
||||
"usersMentioned": "User mentioned",
|
||||
"actor": "Actor",
|
||||
"actorDescription": "The thing that caused this action. <hr class=\"sep\"> \"user\"/\"admin\"/\"daemon\" or a username.",
|
||||
"actorDescription": "The thing that caused this action. \"user\"/\"admin\"/\"daemon\" or a username.",
|
||||
"accountCreationFilter": "Account Creation",
|
||||
"accountDeletionFilter": "Account Deletion",
|
||||
"accountDisabledFilter": "Account Disabled",
|
||||
@ -166,7 +167,9 @@
|
||||
"passwordChangeFilter": "Password Changed",
|
||||
"passwordResetFilter": "Password Reset",
|
||||
"inviteCreatedFilter": "Invite Created",
|
||||
"inviteDeletedFilter": "Invite Deleted/Expired"
|
||||
"inviteDeletedFilter": "Invite Deleted/Expired",
|
||||
"loadMore": "Load More",
|
||||
"noMoreResults": "No more results."
|
||||
},
|
||||
"notifications": {
|
||||
"changedEmailAddress": "Changed email address of {n}.",
|
||||
|
@ -453,4 +453,5 @@ type GetActivitiesDTO struct {
|
||||
|
||||
type GetActivitiesRespDTO struct {
|
||||
Activities []ActivityDTO `json:"activities"`
|
||||
LastPage bool `json:"last_page"`
|
||||
}
|
||||
|
@ -1808,7 +1808,7 @@ export class accountsList {
|
||||
queries: this._queries,
|
||||
setVisibility: this.setVisibility,
|
||||
clearSearchButtonSelector: ".accounts-search-clear",
|
||||
onSearchCallback: () => {
|
||||
onSearchCallback: (_0: number, _1: boolean) => {
|
||||
this._checkCheckCount();
|
||||
}
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { _post, _delete, toDateString } from "../modules/common.js";
|
||||
import { _post, _delete, toDateString, addLoader, removeLoader } from "../modules/common.js";
|
||||
import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js";
|
||||
|
||||
export interface activity {
|
||||
@ -31,7 +31,7 @@ var activityTypeMoods = {
|
||||
|
||||
export var activityReload = new CustomEvent("activity-reload");
|
||||
|
||||
export class Activity implements activity, SearchableItem { // FIXME: Add "implements"
|
||||
export class Activity implements activity, SearchableItem {
|
||||
private _card: HTMLElement;
|
||||
private _title: HTMLElement;
|
||||
private _time: HTMLElement;
|
||||
@ -173,14 +173,6 @@ export class Activity implements activity, SearchableItem { // FIXME: Add "imple
|
||||
|
||||
this._title.innerHTML = innerHTML.replace("{invite}", this._renderInvText());
|
||||
}
|
||||
|
||||
/*} else if (this.source_type == "admin") {
|
||||
// FIXME: Handle contactLinked/Unlinked, creation/deletion, enable/disable, createInvite/deleteInvite
|
||||
} else if (this.source_type == "anon") {
|
||||
this._referrer.innerHTML = ``;
|
||||
} else if (this.source_type == "daemon") {
|
||||
// FIXME: Handle deleteInvite, disabled, deletion
|
||||
}*/
|
||||
}
|
||||
|
||||
get time(): number { return this._timeUnix; }
|
||||
@ -289,7 +281,6 @@ export class Activity implements activity, SearchableItem { // FIXME: Add "imple
|
||||
}
|
||||
|
||||
update = (act: activity) => {
|
||||
// FIXME
|
||||
this._act = act;
|
||||
this.source_type = act.source_type;
|
||||
this.invite_code = act.invite_code;
|
||||
@ -312,6 +303,7 @@ export class Activity implements activity, SearchableItem { // FIXME: Add "imple
|
||||
|
||||
interface ActivitiesDTO {
|
||||
activities: activity[];
|
||||
last_page: boolean;
|
||||
}
|
||||
|
||||
export class activityList {
|
||||
@ -323,7 +315,16 @@ export class activityList {
|
||||
private _sortingByButton = document.getElementById("activity-sort-by-field") as HTMLButtonElement;
|
||||
private _notFoundPanel = document.getElementById("activity-not-found");
|
||||
private _searchBox = document.getElementById("activity-search") as HTMLInputElement;
|
||||
private _sortDirection = document.getElementById("activity-sort-direction") as HTMLButtonElement;
|
||||
private _loader = document.getElementById("activity-loader");
|
||||
private _loadMoreButton = document.getElementById("activity-load-more") as HTMLButtonElement;
|
||||
private _refreshButton = document.getElementById("activity-refresh") as HTMLButtonElement;
|
||||
private _search: Search;
|
||||
private _ascending: boolean;
|
||||
private _hasLoaded: boolean;
|
||||
private _lastLoad: number;
|
||||
private _page: number = 0;
|
||||
private _lastPage: boolean;
|
||||
|
||||
|
||||
setVisibility = (activities: string[], visible: boolean) => {
|
||||
@ -338,12 +339,70 @@ export class activityList {
|
||||
}
|
||||
|
||||
reload = () => {
|
||||
this._lastLoad = Date.now();
|
||||
this._lastPage = false;
|
||||
// this._page = 0;
|
||||
let limit = 10;
|
||||
if (this._page != 0) {
|
||||
limit *= this._page+1;
|
||||
};
|
||||
|
||||
let send = {
|
||||
"type": [],
|
||||
"limit": 60,
|
||||
"limit": limit,
|
||||
"page": 0,
|
||||
"ascending": false
|
||||
"ascending": this.ascending
|
||||
}
|
||||
|
||||
_post("/activity", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4) return;
|
||||
if (req.status != 200) {
|
||||
window.notifications.customError("loadActivitiesError", window.lang.notif("errorLoadActivities"));
|
||||
return;
|
||||
}
|
||||
|
||||
this._hasLoaded = true;
|
||||
// Allow refreshes every 15s
|
||||
this._refreshButton.disabled = true;
|
||||
setTimeout(() => this._refreshButton.disabled = false, 15000);
|
||||
|
||||
let resp = req.response as ActivitiesDTO;
|
||||
// FIXME: Don't destroy everything each reload!
|
||||
this._activities = {};
|
||||
this._ordering = [];
|
||||
|
||||
for (let act of resp.activities) {
|
||||
this._activities[act.id] = new Activity(act);
|
||||
this._ordering.push(act.id);
|
||||
}
|
||||
this._search.items = this._activities;
|
||||
this._search.ordering = this._ordering;
|
||||
|
||||
if (this._search.inSearch) {
|
||||
this._search.onSearchBoxChange(true);
|
||||
} else {
|
||||
this.setVisibility(this._ordering, true);
|
||||
this._notFoundPanel.classList.add("unfocused");
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
this._lastLoad = Date.now();
|
||||
this._loadMoreButton.disabled = true;
|
||||
const timeout = setTimeout(() => this._loadMoreButton.disabled = false, 1000);
|
||||
this._page += 1;
|
||||
|
||||
let send = {
|
||||
"type": [],
|
||||
"limit": 10,
|
||||
"page": this._page,
|
||||
"ascending": this._ascending
|
||||
};
|
||||
|
||||
// this._activityList.classList.add("unfocused");
|
||||
// addLoader(this._loader, false, true);
|
||||
|
||||
_post("/activity", send, (req: XMLHttpRequest) => {
|
||||
if (req.readyState != 4) return;
|
||||
if (req.status != 200) {
|
||||
@ -352,32 +411,32 @@ export class activityList {
|
||||
}
|
||||
|
||||
let resp = req.response as ActivitiesDTO;
|
||||
// FIXME: Don't destroy everything each reload!
|
||||
this._activities = {};
|
||||
|
||||
this._lastPage = resp.last_page;
|
||||
if (this._lastPage) {
|
||||
clearTimeout(timeout);
|
||||
this._loadMoreButton.disabled = true;
|
||||
this._loadMoreButton.textContent = window.lang.strings("noMoreResults");
|
||||
}
|
||||
|
||||
for (let act of resp.activities) {
|
||||
this._activities[act.id] = new Activity(act);
|
||||
this._activityList.appendChild(this._activities[act.id].asElement());
|
||||
this._ordering.push(act.id);
|
||||
}
|
||||
this._search.items = this._activities;
|
||||
// FIXME: Actually implement sorting
|
||||
this._ordering = Object.keys(this._activities);
|
||||
this._search.ordering = this._ordering;
|
||||
// this._search.items = this._activities;
|
||||
// this._search.ordering = this._ordering;
|
||||
|
||||
if (this._search.inSearch) {
|
||||
const results = this._search.search(this._searchBox.value);
|
||||
this.setVisibility(results, true);
|
||||
if (results.length == 0) {
|
||||
this._notFoundPanel.classList.remove("unfocused");
|
||||
} else {
|
||||
this._notFoundPanel.classList.add("unfocused");
|
||||
}
|
||||
this._search.onSearchBoxChange(true);
|
||||
} else {
|
||||
this.setVisibility(this._ordering, true);
|
||||
this._notFoundPanel.classList.add("unfocused");
|
||||
}
|
||||
// removeLoader(this._loader);
|
||||
// this._activityList.classList.remove("unfocused");
|
||||
}, true);
|
||||
}
|
||||
|
||||
|
||||
private _queries: { [field: string]: QueryType } = {
|
||||
"id": {
|
||||
name: window.lang.strings("activityID"),
|
||||
@ -494,6 +553,27 @@ export class activityList {
|
||||
}
|
||||
};
|
||||
|
||||
get ascending(): boolean { return this._ascending; }
|
||||
set ascending(v: boolean) {
|
||||
this._ascending = v;
|
||||
this._sortDirection.innerHTML = `${window.lang.strings("sortDirection")} <i class="ri-arrow-${v ? "up" : "down"}-s-line ml-2"></i>`;
|
||||
if (this._hasLoaded) {
|
||||
this.reload();
|
||||
}
|
||||
}
|
||||
|
||||
detectScroll = () => {
|
||||
// console.log(window.innerHeight + document.documentElement.scrollTop, document.scrollingElement.scrollHeight);
|
||||
if (Math.abs(window.innerHeight + document.documentElement.scrollTop - document.scrollingElement.scrollHeight) < 50) {
|
||||
// window.notifications.customSuccess("scroll", "Reached bottom.");
|
||||
// Wait 1s between loads
|
||||
if (this._lastLoad + 1000 > Date.now()) return;
|
||||
this.loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
private _prevResultCount = 0;
|
||||
|
||||
constructor() {
|
||||
this._activityList = document.getElementById("activity-card-list");
|
||||
document.addEventListener("activity-reload", this.reload);
|
||||
@ -508,9 +588,24 @@ export class activityList {
|
||||
queries: this._queries,
|
||||
setVisibility: this.setVisibility,
|
||||
filterList: document.getElementById("activity-filter-list"),
|
||||
onSearchCallback: () => {}
|
||||
onSearchCallback: (visibleCount: number, newItems: boolean) => {
|
||||
|
||||
if (visibleCount < 10) {
|
||||
if (!newItems || this._prevResultCount != visibleCount || (visibleCount == 0 && !this._lastPage)) this.loadMore();
|
||||
}
|
||||
this._prevResultCount = visibleCount;
|
||||
}
|
||||
}
|
||||
this._search = new Search(conf);
|
||||
this._search.generateFilterList();
|
||||
|
||||
this._hasLoaded = false;
|
||||
this.ascending = false;
|
||||
this._sortDirection.addEventListener("click", () => this.ascending = !this.ascending);
|
||||
|
||||
this._loadMoreButton.onclick = this.loadMore;
|
||||
this._refreshButton.onclick = this.reload;
|
||||
|
||||
window.onscroll = this.detectScroll;
|
||||
}
|
||||
}
|
||||
|
@ -199,9 +199,10 @@ export function toggleLoader(el: HTMLElement, small: boolean = true) {
|
||||
}
|
||||
}
|
||||
|
||||
export function addLoader(el: HTMLElement, small: boolean = true) {
|
||||
export function addLoader(el: HTMLElement, small: boolean = true, relative: boolean = false) {
|
||||
if (!el.classList.contains("loader")) {
|
||||
el.classList.add("loader");
|
||||
if (relative) el.classList.add("rel");
|
||||
if (small) { el.classList.add("loader-sm"); }
|
||||
const dot = document.createElement("span") as HTMLSpanElement;
|
||||
dot.classList.add("dot")
|
||||
@ -213,6 +214,7 @@ export function removeLoader(el: HTMLElement, small: boolean = true) {
|
||||
if (el.classList.contains("loader")) {
|
||||
el.classList.remove("loader");
|
||||
el.classList.remove("loader-sm");
|
||||
el.classList.remove("rel");
|
||||
const dot = el.querySelector("span.dot");
|
||||
if (dot) { dot.remove(); }
|
||||
}
|
||||
|
@ -21,7 +21,8 @@ export interface SearchConfiguration {
|
||||
search: HTMLInputElement;
|
||||
queries: { [field: string]: QueryType };
|
||||
setVisibility: (items: string[], visible: boolean) => void;
|
||||
onSearchCallback: () => void;
|
||||
onSearchCallback: (visibleCount: number, newItems: boolean) => void;
|
||||
loadMore?: () => void;
|
||||
}
|
||||
|
||||
export interface SearchableItem {
|
||||
@ -267,7 +268,7 @@ export class Search {
|
||||
get ordering(): string[] { return this._ordering; }
|
||||
set ordering(v: string[]) { this._ordering = v; }
|
||||
|
||||
onSearchBoxChange = () => {
|
||||
onSearchBoxChange = (newItems: boolean = false) => {
|
||||
const query = this._c.search.value;
|
||||
if (!query) {
|
||||
this.inSearch = false;
|
||||
@ -276,7 +277,7 @@ export class Search {
|
||||
}
|
||||
const results = this.search(query);
|
||||
this._c.setVisibility(results, true);
|
||||
this._c.onSearchCallback();
|
||||
this._c.onSearchCallback(results.length, newItems);
|
||||
this.showHideSearchOptionsHeader();
|
||||
if (results.length == 0) {
|
||||
this._c.notFoundPanel.classList.remove("unfocused");
|
||||
@ -294,6 +295,8 @@ export class Search {
|
||||
this._c.search.setSelectionRange(newPos, newPos);
|
||||
this._c.search.oninput(null as any);
|
||||
};
|
||||
|
||||
|
||||
|
||||
generateFilterList = () => {
|
||||
// Generate filter buttons
|
||||
@ -372,7 +375,7 @@ export class Search {
|
||||
constructor(c: SearchConfiguration) {
|
||||
this._c = c;
|
||||
|
||||
this._c.search.oninput = this.onSearchBoxChange;
|
||||
this._c.search.oninput = () => this.onSearchBoxChange();
|
||||
|
||||
const clearSearchButtons = Array.from(document.querySelectorAll(this._c.clearSearchButtonSelector)) as Array<HTMLSpanElement>;
|
||||
for (let b of clearSearchButtons) {
|
||||
|
Loading…
Reference in New Issue
Block a user