Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions src/app/community/community.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog';
import { UntypedFormBuilder } from '@angular/forms';
import { Subject, forkJoin, iif, of, throwError } from 'rxjs';
import { takeUntil, finalize, switchMap, map, catchError, tap, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { takeUntil, finalize, switchMap, map, catchError, tap, debounceTime, distinctUntilChanged, take } from 'rxjs/operators';
import { StateService } from '../shared/state.service';
import { NewsService } from '../news/news.service';
import { DialogsFormService } from '../shared/dialogs/dialogs-form.service';
Expand Down Expand Up @@ -69,6 +69,7 @@ export class CommunityComponent implements OnInit, OnDestroy {
availableLabels: string[] = [];
selectedLabel = '';
pinned = false;
attachmentMap: Record<string, any> = {};

get leadersTabLabel(): string {
return this.configuration.planetType === 'nation' ? $localize`Nation Leaders` : $localize`Community Leaders`;
Expand Down Expand Up @@ -108,7 +109,6 @@ export class CommunityComponent implements OnInit, OnDestroy {
const newsSortValue = (item: any) => item.sharedDate || item.doc.time;
this.newsService.newsUpdated$.pipe(takeUntil(this.onDestroy$)).subscribe(news => {
this.news = news.sort((a, b) => newsSortValue(b) - newsSortValue(a));
this.news.forEach(item => item.doc.messageLower = item.doc.message?.toLowerCase() || '');
this.filteredNews = this.news;
this.availableLabels = this.getAvailableLabels(this.news);
this.isLoading = false;
Expand Down Expand Up @@ -291,9 +291,13 @@ export class CommunityComponent implements OnInit, OnDestroy {

getLinks(planetCode?) {
return this.teamsService.getTeamMembers(this.team || this.teamObject(planetCode), true).pipe(map((docs) => {
const { link: links, transaction: finances, report: reports } = docs.reduce((docObject, doc) => ({
...docObject, [doc.docType]: [ ...(docObject[doc.docType] || []), doc ]
}), { link: [], transaction: [] });
const { link: links, transaction: finances, report: reports } = docs.reduce((docObject, doc) => {
if (!docObject[doc.docType]) {
docObject[doc.docType] = [];
}
docObject[doc.docType].push(doc);
return docObject;
}, { link: [], transaction: [], report: [] });
return { links, finances, reports };
}));
}
Expand All @@ -312,21 +316,40 @@ export class CommunityComponent implements OnInit, OnDestroy {

setCouncillors(users) {
const planetCode = this.planetCode ? this.planetCode : this.stateService.configuration.code;
this.couchService.findAll('attachments').subscribe((attachments: any[]) => {
this.councillors = users
.filter(user => planetCode === user.doc.planetCode && (user.doc.isUserAdmin || user.doc.roles.indexOf('leader')) !== -1)
.map(user => {
const { _id: userId, planetCode: userPlanetCode, name } = user.doc;
const attachmentId = `org.couchdb.user:${name}@${userPlanetCode}`;
const attachmentDoc: any = attachments.find(attachment => attachment._id === attachmentId);
const avatar = attachmentDoc ?
`${environment.couchAddress}/attachments/${attachmentId}/${Object.keys(attachmentDoc._attachments)[0]}` :
(user.imageSrc || 'assets/image.png');
return { avatar, userDoc: user, userId, name: user.doc.name, userPlanetCode: user.doc.planetCode, ...user };
});
const councillorUsers = users
.filter(user => planetCode === user.doc.planetCode && (user.doc.isUserAdmin || user.doc.roles.indexOf('leader')) !== -1);
const attachmentIds = councillorUsers
.map(user => `org.couchdb.user:${user.doc.name}@${user.doc.planetCode}`)
.filter(id => !!id);

this.fetchMissingAttachments(attachmentIds).pipe(take(1)).subscribe(() => {
this.councillors = councillorUsers.map(user => {
const { _id: userId, planetCode: userPlanetCode, name } = user.doc;
const attachmentId = `org.couchdb.user:${name}@${userPlanetCode}`;
const attachmentDoc: any = this.attachmentMap[attachmentId];
const avatar = attachmentDoc ?
`${environment.couchAddress}/attachments/${attachmentId}/${Object.keys(attachmentDoc._attachments)[0]}` :
(user.imageSrc || 'assets/image.png');
return { avatar, userDoc: user, userId, name: user.doc.name, userPlanetCode: user.doc.planetCode, ...user };
});
});
}

private fetchMissingAttachments(ids: string[]) {
const missing = ids.filter(id => !this.attachmentMap[id]);
if (missing.length === 0) {
return of(undefined);
}
return this.couchService.findAttachmentsByIds(missing).pipe(
tap((attachments: any[]) => {
attachments.forEach(attachment => {
this.attachmentMap[attachment._id] = attachment;
});
}),
map(() => undefined)
);
}

openAddLinkDialog() {
this.dialog.open(CommunityLinkDialogComponent, {
width: '50vw',
Expand Down Expand Up @@ -498,7 +521,12 @@ export class CommunityComponent implements OnInit, OnDestroy {
}
if (this.voiceSearch) {
const lower = this.voiceSearch.toLowerCase();
filtered = filtered.filter(item => item.doc.messageLower.includes(lower));
filtered = filtered.filter(item => {
if (typeof item.doc.messageLower !== 'string') {
item.doc.messageLower = (item.doc.message || '').toLowerCase();
}
return item.doc.messageLower.includes(lower);
});
}
this.filteredNews = filtered;
}
Expand Down
49 changes: 32 additions & 17 deletions src/app/news/news-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export class NewsListComponent implements OnInit, OnChanges, AfterViewInit, OnDe
) {}

ngOnInit() {

this.router.events.subscribe(() => {
this.initNews();
});
Expand All @@ -68,17 +67,22 @@ export class NewsListComponent implements OnInit, OnChanges, AfterViewInit, OnDe
}

ngOnChanges() {
this.isLoadingMore = false;
let isLatest = true;
this.replyObject = {};
this.items.forEach(item => {
this.replyObject[item.doc.replyTo || 'root'] = [ ...(this.replyObject[item.doc.replyTo || 'root'] || []), item ];
const key = item.doc.replyTo || 'root';
if (!this.replyObject[key]) {
this.replyObject[key] = [];
}
this.replyObject[key].push(item);
if (!item.doc.replyTo && isLatest) {
item.latestMessage = true;
isLatest = false;
}
});
this.displayedItems = this.replyObject[this.replyViewing._id];
this.loadPagedItems(true);
this.updateDisplayedItems(true);
if (this.replyViewing._id !== 'root') {
this.replyViewing = this.items.find(item => item._id === this.replyViewing._id);
}
Expand Down Expand Up @@ -146,7 +150,7 @@ export class NewsListComponent implements OnInit, OnChanges, AfterViewInit, OnDe
const news = this.items.find(item => item._id === newsId) || { _id: 'root' };
this.replyViewing = news;
this.displayedItems = this.replyObject[news._id];
this.loadPagedItems(true);
this.updateDisplayedItems(true);
this.isMainPostShared = this.replyViewing._id === 'root' || this.newsService.postSharedWithCommunity(this.replyViewing);
this.showMainPostShare = !this.replyViewing.doc || !this.replyViewing.doc.replyTo ||
(
Expand Down Expand Up @@ -282,27 +286,38 @@ export class NewsListComponent implements OnInit, OnChanges, AfterViewInit, OnDe
};
}

loadPagedItems(initial = true) {
let pageSize = this.pageSize;
if (initial) {
loadMoreItems() {
if (this.isLoadingMore) {
return;
}
this.updateDisplayedItems();
}

private updateDisplayedItems(reset = false) {
const news = this.getCurrentItems();
if (reset) {
this.displayedItems = [];
this.nextStartIndex = 0;
// Take maximum so if fewer posts than page size adding a post doesn't add a "Load More" button
pageSize = Math.max(this.pageEnd[this.replyViewing._id] || this.pageSize, this.pageSize);
}
const news = this.getCurrentItems();

const pageSize = reset ? Math.max(this.pageEnd[this.replyViewing._id] || this.pageSize, this.pageSize) : this.pageSize;
const { items, endIndex, hasMore } = this.paginateItems(news, this.nextStartIndex, pageSize);

this.displayedItems = [ ...this.displayedItems, ...items ];
this.displayedItems = reset ? items : [ ...this.displayedItems, ...items ];
this.pageEnd[this.replyViewing._id] = this.displayedItems.length;
this.nextStartIndex = endIndex;
this.hasMoreNews = hasMore;
this.isLoadingMore = false;
this.totalReplies = news.length;
}

loadMoreItems() {
this.isLoadingMore = true;
this.loadPagedItems(false);
const reachedLocalEnd = endIndex >= news.length;
const shouldFetchRemote = reachedLocalEnd && !hasMore && this.newsService.canLoadMore();

if (shouldFetchRemote) {
this.isLoadingMore = true;
this.newsService.loadMoreNews();
} else {
this.isLoadingMore = false;
}

this.hasMoreNews = hasMore || shouldFetchRemote || this.newsService.canLoadMore();
}
}
113 changes: 101 additions & 12 deletions src/app/news/news.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { Subject, forkJoin, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { of, Subject } from 'rxjs';
import { CouchService } from '../shared/couchdb.service';
import { StateService } from '../shared/state.service';
import { UserService } from '../shared/user.service';
Expand All @@ -9,6 +8,7 @@ import { findDocuments } from '../shared/mangoQueries';
import { environment } from '../../environments/environment';
import { dedupeObjectArray } from '../shared/utils';
import { planetAndParentId } from '../manager-dashboard/reports/reports.utils';
import { map } from 'rxjs/operators';

@Injectable({
providedIn: 'root'
Expand All @@ -19,6 +19,12 @@ export class NewsService {
imgUrlPrefix = environment.couchAddress;
newsUpdated$ = new Subject<any[]>();
currentOptions: { selectors: any, viewId: string } = { selectors: {}, viewId: '' };
private currentBookmark: string | undefined;
private hasMoreResults = true;
private isLoading = false;
private pageSize = 50;
private accumulatedNews: any[] = [];
private avatarCache: Record<string, any> = {};

constructor(
private couchService: CouchService,
Expand All @@ -29,19 +35,22 @@ export class NewsService {

requestNews({ selectors, viewId } = this.currentOptions) {
this.currentOptions = { selectors, viewId };
forkJoin([
this.couchService.findAll(this.dbName, findDocuments(selectors, 0, [ { 'time': 'desc' } ])),
this.couchService.findAll('attachments')
]).subscribe(([ newsItems, avatars ]) => {
this.newsUpdated$.next(newsItems.map((item: any) => (
{ doc: item, sharedDate: this.findShareDate(item, viewId), avatar: this.findAvatar(item.user, avatars), _id: item._id }
)));
});
this.resetPagination();
this.fetchNewsPage(true);
}

loadMoreNews() {
if (!this.canLoadMore()) { return; }
this.fetchNewsPage(false);
}

canLoadMore(): boolean {
return this.hasMoreResults && !this.isLoading;
}

findAvatar(user: any, attachments: any[]) {
findAvatar(user: any, attachments: Record<string, any>) {
const attachmentId = `${user._id}@${user.planetCode}`;
const attachment = attachments.find(avatar => avatar._id === attachmentId);
const attachment = attachments ? attachments[attachmentId] : undefined;
const extractFilename = (object) => Object.keys(object._attachments)[0];
return attachment ?
`${this.imgUrlPrefix}/attachments/${attachmentId}/${extractFilename(attachment)}` :
Expand All @@ -54,6 +63,86 @@ export class NewsService {
return ((item.viewIn || []).find(view => view._id === viewId) || {}).sharedDate;
}

private collectAttachmentIds(newsItems: any[]): string[] {
const ids = new Set<string>();
newsItems.forEach((item: any) => {
const user = item?.user;
if (user && user._id && user.planetCode) {
ids.add(`${user._id}@${user.planetCode}`);
}
});
return Array.from(ids);
}

private resetPagination() {
this.currentBookmark = undefined;
this.hasMoreResults = true;
this.isLoading = false;
this.accumulatedNews = [];
this.avatarCache = {};
}

private fetchNewsPage(reset: boolean) {
const { selectors } = this.currentOptions;
const baseQuery = findDocuments(selectors, 0, [ { 'time': 'desc' } ], this.pageSize);
const query: any = { ...baseQuery };
if (!reset && this.currentBookmark) {
query.bookmark = this.currentBookmark;
}
this.isLoading = true;
this.couchService.post(`${this.dbName}/_find`, query).subscribe({
next: (response: any) => {
const docs = response?.docs ?? [];
const bookmark = response?.bookmark;
this.isLoading = false;
this.currentBookmark = bookmark;
if (reset) {
this.accumulatedNews = [];
}
if (docs.length === 0) {
this.hasMoreResults = false;
this.emitNews();
return;
}
this.hasMoreResults = docs.length === this.pageSize && !!bookmark;
this.accumulatedNews = [ ...this.accumulatedNews, ...docs ];
this.emitNews();
this.loadMissingAttachments(docs);
},
error: () => {
this.isLoading = false;
this.hasMoreResults = false;
}
});
}

private loadMissingAttachments(newsItems: any[]) {
const missingIds = this.collectAttachmentIds(newsItems).filter(id => !(id in this.avatarCache));
if (missingIds.length === 0) {
return;
}
this.couchService.findAttachmentsByIds(missingIds).subscribe((attachments: any[]) => {
if (!attachments || attachments.length === 0) { return; }
attachments.forEach((attachment: any) => {
if (attachment && attachment._id) {
this.avatarCache[attachment._id] = attachment;
}
});
this.emitNews();
});
}

private emitNews() {
const viewId = this.currentOptions.viewId;
const formatted = this.accumulatedNews.map((item: any) => ({
doc: item,
sharedDate: this.findShareDate(item, viewId),
avatar: this.findAvatar(item.user, this.avatarCache),
_id: item._id
}));
this.newsUpdated$.next([ ...formatted ]);
}

postNews(post, successMessage = $localize`Thank you for submitting your message`, isMessageEdit = true) {
const { configuration } = this.stateService;
const message = typeof post.message === 'string' ? post.message : post.message.text;
Expand Down
Loading
Loading