diff --git a/appveyor.yml b/appveyor.yml index 5e1b8a8d..a3657f4c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -30,6 +30,8 @@ test_script: $wc.UploadFile("https://ci.appveyor.com/api/testresults/junit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $_)) } - npm run dist +- ps: >- + Remove-Item 'C:\projects\sengi\dist\assets\emoji' -Recurse artifacts: - path: dist deploy: @@ -42,4 +44,4 @@ deploy: folder: / application: dist.zip on: - branch: master \ No newline at end of file + branch: master diff --git a/package.json b/package.json index f0d7ab76..723b0b16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sengi", - "version": "0.21.0", + "version": "0.22.0", "license": "AGPL-3.0-or-later", "main": "main-electron.js", "description": "A multi-account desktop client for Mastodon and Pleroma", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 15967d66..a8ebd174 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -81,6 +81,7 @@ import { StreamNotificationsComponent } from './components/stream/stream-notific import { NotificationComponent } from './components/floating-column/manage-account/notifications/notification/notification.component'; import { ServiceWorkerModule } from '@angular/service-worker'; import { environment } from '../environments/environment'; +import { BookmarksComponent } from './components/floating-column/manage-account/bookmarks/bookmarks.component'; const routes: Routes = [ @@ -142,7 +143,8 @@ const routes: Routes = [ ScheduledStatusesComponent, ScheduledStatusComponent, StreamNotificationsComponent, - NotificationComponent + NotificationComponent, + BookmarksComponent ], entryComponents: [ EmojiPickerComponent diff --git a/src/app/components/floating-column/floating-column.component.html b/src/app/components/floating-column/floating-column.component.html index d2672e09..330e49fd 100644 --- a/src/app/components/floating-column/floating-column.component.html +++ b/src/app/components/floating-column/floating-column.component.html @@ -15,13 +15,16 @@ - + diff --git a/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.html b/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.html new file mode 100644 index 00000000..828f406c --- /dev/null +++ b/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.html @@ -0,0 +1,3 @@ +

+ bookmarks works! +

diff --git a/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.scss b/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/pages/register-new-account/register-new-account.component.spec.ts b/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.spec.ts similarity index 50% rename from src/app/pages/register-new-account/register-new-account.component.spec.ts rename to src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.spec.ts index 449537d4..c0d45e98 100644 --- a/src/app/pages/register-new-account/register-new-account.component.spec.ts +++ b/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.spec.ts @@ -1,20 +1,20 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { RegisterNewAccountComponent } from './register-new-account.component'; +import { BookmarksComponent } from './bookmarks.component'; -xdescribe('RegisterNewAccountComponent', () => { - let component: RegisterNewAccountComponent; - let fixture: ComponentFixture; +xdescribe('BookmarksComponent', () => { + let component: BookmarksComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ RegisterNewAccountComponent ] + declarations: [ BookmarksComponent ] }) .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(RegisterNewAccountComponent); + fixture = TestBed.createComponent(BookmarksComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.ts b/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.ts new file mode 100644 index 00000000..87f6aad1 --- /dev/null +++ b/src/app/components/floating-column/manage-account/bookmarks/bookmarks.component.ts @@ -0,0 +1,125 @@ +import { Component, OnInit, Output, EventEmitter, Input, ViewChild, ElementRef } from '@angular/core'; + +import { StatusWrapper } from '../../../../models/common.model'; +import { OpenThreadEvent } from '../../../../services/tools.service'; +import { AccountWrapper } from '../../../../models/account.models'; +import { FavoriteResult, BookmarkResult } from '../../../../services/mastodon.service'; +import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service'; +import { Status } from '../../../../services/models/mastodon.interfaces'; +import { NotificationService } from '../../../../services/notification.service'; + +@Component({ + selector: 'app-bookmarks', + templateUrl: '../../../stream/stream-statuses/stream-statuses.component.html', + styleUrls: ['../../../stream/stream-statuses/stream-statuses.component.scss', './bookmarks.component.scss'] +}) +export class BookmarksComponent implements OnInit { + statuses: StatusWrapper[] = []; + displayError: string; + isLoading = true; + isThread = false; + hasContentWarnings = false; + + bufferStream: Status[] = []; //html compatibility only + + @Output() browseAccountEvent = new EventEmitter(); + @Output() browseHashtagEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); + + private maxReached = false; + private maxId: string; + private _account: AccountWrapper; + + @Input('account') + set account(acc: AccountWrapper) { + this._account = acc; + this.getBookmarks(); + } + get account(): AccountWrapper { + return this._account; + } + + @ViewChild('statusstream') public statustream: ElementRef; + + + constructor( + private readonly notificationService: NotificationService, + private readonly mastodonService: MastodonWrapperService) { } + + ngOnInit() { + } + + private reset() { + this.isLoading = true; + this.statuses.length = 0; + this.maxReached = false; + this.maxId = null; + } + + private getBookmarks() { + this.reset(); + + this.mastodonService.getBookmarks(this.account.info) + .then((result: BookmarkResult) => { + this.maxId = result.max_id; + for (const s of result.bookmarked) { + const wrapper = new StatusWrapper(s, this.account.info); + this.statuses.push(wrapper); + } + }) + .catch(err => { + this.notificationService.notifyHttpError(err, this.account.info); + }) + .then(() => { + this.isLoading = false; + }); + } + + onScroll() { + var element = this.statustream.nativeElement as HTMLElement; + const atBottom = element.scrollHeight <= element.clientHeight + element.scrollTop + 1000; + + if (atBottom) { + this.scrolledToBottom(); + } + } + + + private scrolledToBottom() { + if (this.isLoading || this.maxReached) return; + + this.isLoading = true; + this.mastodonService.getBookmarks(this.account.info, this.maxId) + .then((result: BookmarkResult) => { + const statuses = result.bookmarked; + if (statuses.length === 0 || !this.maxId) { + this.maxReached = true; + return; + } + + this.maxId = result.max_id; + for (const s of statuses) { + const wrapper = new StatusWrapper(s, this.account.info); + this.statuses.push(wrapper); + } + }) + .catch(err => { + this.notificationService.notifyHttpError(err, this.account.info); + }) + .then(() => { + this.isLoading = false; + }); + } + + browseAccount(accountName: string): void { + this.browseAccountEvent.next(accountName); + } + + browseHashtag(hashtag: string): void { + this.browseHashtagEvent.next(hashtag); + } + + browseThread(openThreadEvent: OpenThreadEvent): void { + this.browseThreadEvent.next(openThreadEvent); + } +} diff --git a/src/app/components/floating-column/manage-account/favorites/favorites.component.ts b/src/app/components/floating-column/manage-account/favorites/favorites.component.ts index 66d31547..a40efc13 100644 --- a/src/app/components/floating-column/manage-account/favorites/favorites.component.ts +++ b/src/app/components/floating-column/manage-account/favorites/favorites.component.ts @@ -7,7 +7,6 @@ import { FavoriteResult } from '../../../../services/mastodon.service'; import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service'; import { Status } from '../../../../services/models/mastodon.interfaces'; import { NotificationService } from '../../../../services/notification.service'; -import { resetCompiledComponents } from '@angular/core/src/render3/jit/module'; @Component({ selector: 'app-favorites', diff --git a/src/app/components/floating-column/manage-account/manage-account.component.html b/src/app/components/floating-column/manage-account/manage-account.component.html index b6e92d85..4ad23889 100644 --- a/src/app/components/floating-column/manage-account/manage-account.component.html +++ b/src/app/components/floating-column/manage-account/manage-account.component.html @@ -3,10 +3,16 @@ + diff --git a/src/app/components/floating-column/manage-account/manage-account.component.ts b/src/app/components/floating-column/manage-account/manage-account.component.ts index 1184994a..c6fdfe69 100644 --- a/src/app/components/floating-column/manage-account/manage-account.component.ts +++ b/src/app/components/floating-column/manage-account/manage-account.component.ts @@ -1,11 +1,11 @@ import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angular/core'; import { faAt, faUserPlus } from "@fortawesome/free-solid-svg-icons"; -import { faBell, faEnvelope, faUser, faStar } from "@fortawesome/free-regular-svg-icons"; +import { faBell, faEnvelope, faUser, faStar, faBookmark } from "@fortawesome/free-regular-svg-icons"; import { Subscription } from 'rxjs'; import { AccountWrapper } from '../../../models/account.models'; import { UserNotificationService, UserNotification } from '../../../services/user-notification.service'; -import { OpenThreadEvent, ToolsService } from '../../../services/tools.service'; +import { OpenThreadEvent, ToolsService, InstanceInfo } from '../../../services/tools.service'; import { MastodonWrapperService } from '../../../services/mastodon-wrapper.service'; import { Account } from "../../../services/models/mastodon.interfaces"; import { NotificationService } from '../../../services/notification.service'; @@ -24,10 +24,12 @@ export class ManageAccountComponent implements OnInit, OnDestroy { faUser = faUser; faStar = faStar; faUserPlus = faUserPlus; + faBookmark = faBookmark; - subPanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' = 'account'; + subPanel: 'account' | 'notifications' | 'mentions' | 'dm' | 'favorites' | 'bookmarks' = 'account'; hasNotifications = false; hasMentions = false; + isBookmarksAvailable = false; userAccount: Account; @@ -38,6 +40,7 @@ export class ManageAccountComponent implements OnInit, OnDestroy { @Input('account') set account(acc: AccountWrapper) { this._account = acc; + this.checkIfBookmarksAreAvailable(); this.checkNotifications(); this.getUserUrl(acc.info); } @@ -54,13 +57,27 @@ export class ManageAccountComponent implements OnInit, OnDestroy { private readonly notificationService: NotificationService, private readonly userNotificationService: UserNotificationService) { } - ngOnInit() { + ngOnInit() { } ngOnDestroy(): void { this.userNotificationServiceSub.unsubscribe(); } + private checkIfBookmarksAreAvailable() { + this.toolsService.getInstanceInfo(this.account.info) + .then((instance: InstanceInfo) => { + if (instance.major >= 3 && instance.minor >= 1) { + this.isBookmarksAvailable = true; + } else { + this.isBookmarksAvailable = false; + } + }) + .catch(err => { + this.isBookmarksAvailable = false; + }); + } + private getUserUrl(account: AccountInfo) { this.mastodonService.retrieveAccountDetails(this.account.info) .then((acc: Account) => { diff --git a/src/app/components/floating-column/search/search.component.html b/src/app/components/floating-column/search/search.component.html index f00a4d4f..0d1f2822 100644 --- a/src/app/components/floating-column/search/search.component.html +++ b/src/app/components/floating-column/search/search.component.html @@ -16,7 +16,7 @@ @@ -19,6 +20,12 @@ + + + + + @@ -28,48 +35,6 @@ - - + \ No newline at end of file diff --git a/src/app/components/stream/status/action-bar/action-bar.component.scss b/src/app/components/stream/status/action-bar/action-bar.component.scss index e49a119d..85eb8cd8 100644 --- a/src/app/components/stream/status/action-bar/action-bar.component.scss +++ b/src/app/components/stream/status/action-bar/action-bar.component.scss @@ -1,7 +1,7 @@ @import "variables"; .action-bar { - // outline: 1px solid greenyellow; // height: 20px; + //outline: 1px solid greenyellow; // height: 20px; margin: 5px 0px 5px $avatar-column-space; padding: 0; font-size: 18px; @@ -10,17 +10,18 @@ // transform: rotate(0.03deg); &__link { + //outline: 1px solid greenyellow; color: $status-secondary-color; + padding: 0 4px; &:hover { color: $status-links-color; } &:not(:last-child) { - margin-right: 15px; + margin-right: 8px; } - &--reply { font-size: 20px; } @@ -34,6 +35,14 @@ &--fav { font-size: 17px; } + + &--bookmark { + display: inline-block; + position: relative; + padding: 0 8px; + // bottom: -1px; + font-size: 16px; + } &--cw { position: relative; @@ -86,6 +95,13 @@ } } +.bookmarked { + color: $bookmarked-color; + + &:hover { + color: darken($bookmarked-color, 10); + } +} @keyframes loadingAnimation { 0% { diff --git a/src/app/components/stream/status/action-bar/action-bar.component.ts b/src/app/components/stream/status/action-bar/action-bar.component.ts index 5b2f2f95..05160c57 100644 --- a/src/app/components/stream/status/action-bar/action-bar.component.ts +++ b/src/app/components/stream/status/action-bar/action-bar.component.ts @@ -2,13 +2,13 @@ import { Component, OnInit, OnDestroy, Input, Output, EventEmitter } from '@angu import { HttpErrorResponse } from '@angular/common/http'; import { Store } from '@ngxs/store'; import { Observable, Subscription } from 'rxjs'; -import { faWindowClose, faReply, faRetweet, faStar, faEllipsisH, faLock, faEnvelope } from "@fortawesome/free-solid-svg-icons"; +import { faWindowClose, faReply, faRetweet, faStar, faEllipsisH, faLock, faEnvelope, faBookmark } from "@fortawesome/free-solid-svg-icons"; import { faWindowClose as faWindowCloseRegular } from "@fortawesome/free-regular-svg-icons"; import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service'; import { AccountInfo } from '../../../../states/accounts.state'; import { Status, Account, Results } from '../../../../services/models/mastodon.interfaces'; -import { ToolsService, OpenThreadEvent } from '../../../../services/tools.service'; +import { ToolsService, OpenThreadEvent, InstanceInfo } from '../../../../services/tools.service'; import { NotificationService } from '../../../../services/notification.service'; import { StatusWrapper } from '../../../../models/common.model'; import { StatusesStateService, StatusState } from '../../../../services/statuses-state.service'; @@ -27,6 +27,7 @@ export class ActionBarComponent implements OnInit, OnDestroy { faEllipsisH = faEllipsisH; faLock = faLock; faEnvelope = faEnvelope; + faBookmark = faBookmark; @Input() statusWrapper: StatusWrapper; @Output() replyEvent = new EventEmitter(); @@ -34,13 +35,16 @@ export class ActionBarComponent implements OnInit, OnDestroy { @Output() browseThreadEvent = new EventEmitter(); + isBookmarked: boolean; isFavorited: boolean; isBoosted: boolean; isDM: boolean; isBoostLocked: boolean; isLocked: boolean; + isBookmarksAvailable: boolean; + bookmarkingIsLoading: boolean; favoriteIsLoading: boolean; boostIsLoading: boolean; @@ -53,6 +57,7 @@ export class ActionBarComponent implements OnInit, OnDestroy { private favoriteStatePerAccountId: { [id: string]: boolean; } = {}; private bootedStatePerAccountId: { [id: string]: boolean; } = {}; + private bookmarkStatePerAccountId: { [id: string]: boolean; } = {}; private accounts$: Observable; private accountSub: Subscription; @@ -69,19 +74,17 @@ export class ActionBarComponent implements OnInit, OnDestroy { } ngOnInit() { - const status = this.statusWrapper.status; + this.displayedStatus = this.statusWrapper.status; const account = this.statusWrapper.provider; - if (status.reblog) { - this.favoriteStatePerAccountId[account.id] = status.reblog.favourited; - this.bootedStatePerAccountId[account.id] = status.reblog.reblogged; - this.displayedStatus = status.reblog; - } else { - this.favoriteStatePerAccountId[account.id] = status.favourited; - this.bootedStatePerAccountId[account.id] = status.reblogged; - this.displayedStatus = status; + if (this.displayedStatus.reblog) { + this.displayedStatus = this.displayedStatus.reblog; } + this.favoriteStatePerAccountId[account.id] = this.displayedStatus.favourited; + this.bootedStatePerAccountId[account.id] = this.displayedStatus.reblogged; + this.bookmarkStatePerAccountId[account.id] = this.displayedStatus.bookmarked; + this.analyseMemoryStatus(); if (this.displayedStatus.visibility === 'direct') { @@ -94,11 +97,20 @@ export class ActionBarComponent implements OnInit, OnDestroy { this.statusStateSub = this.statusStateService.stateNotification.subscribe((state: StatusState) => { if (state && state.statusId === this.displayedStatus.url) { - this.favoriteStatePerAccountId[state.accountId] = state.isFavorited; - this.bootedStatePerAccountId[state.accountId] = state.isRebloged; + + if (state.isFavorited) { + this.favoriteStatePerAccountId[state.accountId] = state.isFavorited; + } + if (state.isRebloged) { + this.bootedStatePerAccountId[state.accountId] = state.isRebloged; + } + if (state.isBookmarked) { + this.bookmarkStatePerAccountId[state.accountId] = state.isBookmarked; + } this.checkIfFavorited(); this.checkIfBoosted(); + this.checkIfBookmarked(); } }); } @@ -115,6 +127,7 @@ export class ActionBarComponent implements OnInit, OnDestroy { memoryStatusState.forEach((state: StatusState) => { this.favoriteStatePerAccountId[state.accountId] = state.isFavorited; this.bootedStatePerAccountId[state.accountId] = state.isRebloged; + this.bookmarkStatePerAccountId[state.accountId] = state.isBookmarked; }); } @@ -140,10 +153,13 @@ export class ActionBarComponent implements OnInit, OnDestroy { this.isContentWarningActive = true; } + this.checkIfBookmarksAreAvailable(this.selectedAccounts[0]); this.checkIfFavorited(); this.checkIfBoosted(); + this.checkIfBookmarked(); } + showContent(): boolean { this.isContentWarningActive = false; this.cwIsActiveEvent.next(false); @@ -236,6 +252,49 @@ export class ActionBarComponent implements OnInit, OnDestroy { return false; } + bookmark(): boolean { + if (this.bookmarkingIsLoading) return; + + this.bookmarkingIsLoading = true; + + const account = this.toolsService.getSelectedAccounts()[0]; + const usableStatus = this.toolsService.getStatusUsableByAccount(account, this.statusWrapper); + usableStatus + .then((status: Status) => { + if (this.isBookmarked && status.bookmarked) { + return this.mastodonService.unbookmark(account, status); + } else if (!this.isBookmarked && !status.bookmarked) { + return this.mastodonService.bookmark(account, status); + } else { + return Promise.resolve(status); + } + }) + .then((bookmarkedStatus: Status) => { + let bookmarked = bookmarkedStatus.bookmarked; //FIXME: when pixelfed will return the good status + if (bookmarked === null) { + bookmarked = !this.bookmarkStatePerAccountId[account.id]; + } + this.bookmarkStatePerAccountId[account.id] = bookmarked; + this.checkIfBookmarked(); + }) + .catch((err: HttpErrorResponse) => { + this.notificationService.notifyHttpError(err, account); + }) + .then(() => { + this.statusStateService.statusBookmarkStatusChanged(this.displayedStatus.url, account.id, this.bookmarkStatePerAccountId[account.id]); + this.bookmarkingIsLoading = false; + }); + + + + // setTimeout(() => { + // this.isBookmarked = !this.isBookmarked; + // this.bookmarkingIsLoading = false; + // }, 2000); + + return false; + } + private checkIfBoosted() { const selectedAccount = this.selectedAccounts[0]; if (selectedAccount) { @@ -255,6 +314,30 @@ export class ActionBarComponent implements OnInit, OnDestroy { } } + private checkIfBookmarked() { + const selectedAccount = this.selectedAccounts[0]; + + if (selectedAccount) { + this.isBookmarked = this.bookmarkStatePerAccountId[selectedAccount.id]; + } else { + this.isBookmarked = false; + } + } + + private checkIfBookmarksAreAvailable(account: AccountInfo) { + this.toolsService.getInstanceInfo(account) + .then((instance: InstanceInfo) => { + if (instance.major >= 3 && instance.minor >= 1) { + this.isBookmarksAvailable = true; + } else { + this.isBookmarksAvailable = false; + } + }) + .catch(err => { + this.isBookmarksAvailable = false; + }); + } + browseThread(event: OpenThreadEvent) { this.browseThreadEvent.next(event); } diff --git a/src/app/components/stream/status/card/card.component.html b/src/app/components/stream/status/card/card.component.html index 7d402c28..3a816859 100644 --- a/src/app/components/stream/status/card/card.component.html +++ b/src/app/components/stream/status/card/card.component.html @@ -1,5 +1,5 @@
- +