created service and account animation #55

This commit is contained in:
Nicolas Constant 2019-03-24 17:52:34 -04:00
parent a8430b0354
commit 22b39b3c53
No known key found for this signature in database
GPG Key ID: 1E9F677FB01A5688
7 changed files with 258 additions and 48 deletions

View File

@ -1,4 +1,5 @@
<a class="account-icon" <a class="account-icon"
href title="{{ account.info.id }}" (click)="toogleAccount()" (contextmenu)="openMenu()"> href title="{{ account.info.id }}" (click)="toogleAccount()" (contextmenu)="openMenu()">
<img class="account-icon__avatar" [class.account-icon__avatar--selected]="account.info.isSelected" src="{{ account.avatar }}" /> <span class="hasActivity" *ngIf="account.hasActivityNotifications">new</span>
<img class="account-icon__avatar" [class.account-icon__avatar--selected]="account.info.isSelected" src="{{ account.avatar }}" />
</a> </a>

View File

@ -1,45 +1,98 @@
.account-icon { .account-icon {
display: inline-block; display: inline-block;
width: 50px; width: 50px; // padding-top: 4px;
// padding-top: 4px;
// margin-left: 5px; // margin-left: 5px;
margin: 0 0 5px 5px; margin: 0 0 5px 5px;
&__avatar { &__avatar {
border-radius: 50%;
border-radius: 2px; border-radius: 2px;
width: 40px; width: 40px;
opacity: .3; opacity: .3;
transition: all .2s; transition: all .2s;
&:hover { &:hover {
filter: alpha(opacity=50); filter: alpha(opacity=50);
opacity: .5; opacity: .5;
} }
&--selected { &--selected {
// border-radius: 20%; // border-radius: 20%;
filter: alpha(opacity=100); filter: alpha(opacity=100);
opacity: 1; opacity: 1;
&:hover { &:hover {
filter: alpha(opacity=100); filter: alpha(opacity=100);
opacity: 1; opacity: 1;
} }
} }
} }
}
// & a { @keyframes flickerAnimation {
// margin-left: 4px; 0% {
// /*margin-top: 4px;*/ opacity: 0;
// } }
// & img { 50% {
// width: 40px; opacity: 1;
// border-radius: 50%; }
// } 100% {
opacity: 0;
}
}
@-o-keyframes flickerAnimation {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-moz-keyframes flickerAnimation {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@-webkit-keyframes flickerAnimation {
0% {
opacity: 0;
}
50% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.hasActivity {
-webkit-animation: flickerAnimation 2s infinite;
-moz-animation: flickerAnimation 2s infinite;
-o-animation: flickerAnimation 2s infinite;
animation: flickerAnimation 2s infinite;
border-radius: 2px;
width: 40px;
height: 40px;
position: absolute;
border: 2px solid orange;
z-index: 20;
color: orange;
font-size: 10px;
font-style: italic;
padding: 23px 0 0 3px;
background: rgba(0,0,0, .55);
&:hover {
color: orange;
}
} }

View File

@ -1,28 +1,30 @@
import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core'; import { Component, OnInit, Input, EventEmitter, Output } from '@angular/core';
import { AccountWrapper } from '../../../models/account.models'; import { AccountWrapper } from '../../../models/account.models';
import { AccountWithNotificationWrapper } from '../left-side-bar.component';
@Component({ @Component({
selector: 'app-account-icon', selector: 'app-account-icon',
templateUrl: './account-icon.component.html', templateUrl: './account-icon.component.html',
styleUrls: ['./account-icon.component.scss'] styleUrls: ['./account-icon.component.scss']
}) })
export class AccountIconComponent implements OnInit { export class AccountIconComponent implements OnInit {
@Input() account: AccountWrapper; @Input() account: AccountWithNotificationWrapper;
@Output() toogleAccountNotify = new EventEmitter<AccountWrapper>(); @Output() toogleAccountNotify = new EventEmitter<AccountWrapper>();
@Output() openMenuNotify = new EventEmitter<AccountWrapper>(); @Output() openMenuNotify = new EventEmitter<AccountWrapper>();
constructor() { } constructor() { }
ngOnInit() { ngOnInit() {
} }
toogleAccount(): boolean { toogleAccount(): boolean {
this.toogleAccountNotify.emit(this.account); this.toogleAccountNotify.emit(this.account);
return false; return false;
} }
openMenu(): boolean { openMenu(): boolean {
this.openMenuNotify.emit(this.account); this.openMenuNotify.emit(this.account);
return false; return false;
} }
} }

View File

@ -10,6 +10,7 @@ import { AccountInfo, SelectAccount } from "../../states/accounts.state";
import { NavigationService, LeftPanelType } from "../../services/navigation.service"; import { NavigationService, LeftPanelType } from "../../services/navigation.service";
import { MastodonService } from "../../services/mastodon.service"; import { MastodonService } from "../../services/mastodon.service";
import { NotificationService } from "../../services/notification.service"; import { NotificationService } from "../../services/notification.service";
import { UserNotificationServiceService, UserNotification } from '../../services/user-notification-service.service';
@Component({ @Component({
selector: "app-left-side-bar", selector: "app-left-side-bar",
@ -19,14 +20,15 @@ import { NotificationService } from "../../services/notification.service";
export class LeftSideBarComponent implements OnInit, OnDestroy { export class LeftSideBarComponent implements OnInit, OnDestroy {
faCommentAlt = faCommentAlt; faCommentAlt = faCommentAlt;
accounts: AccountWrapper[] = []; accounts: AccountWithNotificationWrapper[] = [];
hasAccounts: boolean; hasAccounts: boolean;
private accounts$: Observable<AccountInfo[]>; private accounts$: Observable<AccountInfo[]>;
// private loadedAccounts: { [index: string]: AccountInfo } = {}; private accountSub: Subscription;
private sub: Subscription; private notificationSub: Subscription;
constructor( constructor(
private readonly userNotificationServiceService: UserNotificationServiceService,
private readonly notificationService: NotificationService, private readonly notificationService: NotificationService,
private readonly navigationService: NavigationService, private readonly navigationService: NavigationService,
private readonly mastodonService: MastodonService, private readonly mastodonService: MastodonService,
@ -37,7 +39,7 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
private currentLoading: number; private currentLoading: number;
ngOnInit() { ngOnInit() {
this.accounts$.subscribe((accounts: AccountInfo[]) => { this.accountSub = this.accounts$.subscribe((accounts: AccountInfo[]) => {
if (accounts) { if (accounts) {
//Update and Add //Update and Add
for (let acc of accounts) { for (let acc of accounts) {
@ -45,8 +47,9 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
if (previousAcc) { if (previousAcc) {
previousAcc.info.isSelected = acc.isSelected; previousAcc.info.isSelected = acc.isSelected;
} else { } else {
const accWrapper = new AccountWrapper(); const accWrapper = new AccountWithNotificationWrapper();
accWrapper.info = acc; accWrapper.info = acc;
this.accounts.push(accWrapper); this.accounts.push(accWrapper);
this.mastodonService.retrieveAccountDetails(acc) this.mastodonService.retrieveAccountDetails(acc)
@ -61,17 +64,31 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
//Delete //Delete
const deletedAccounts = this.accounts.filter(x => accounts.findIndex(y => y.id === x.info.id) === -1); const deletedAccounts = this.accounts.filter(x => accounts.findIndex(y => y.id === x.info.id) === -1);
for(let delAcc of deletedAccounts){ for (let delAcc of deletedAccounts) {
this.accounts = this.accounts.filter(x => x.info.id !== delAcc.info.id); this.accounts = this.accounts.filter(x => x.info.id !== delAcc.info.id);
} }
this.hasAccounts = this.accounts.length > 0; this.hasAccounts = this.accounts.length > 0;
} }
}); });
this.notificationSub = this.userNotificationServiceService.userNotifications.subscribe((notifications: UserNotification[]) => {
notifications.forEach((notification: UserNotification) => {
const acc = this.accounts.find(x => x.info.id === notification.account.id);
if(acc){
acc.hasActivityNotifications = notification.hasNewMentions || notification.hasNewNotifications;
}
});
console.warn('new notifications');
console.warn(notifications);
});
} }
ngOnDestroy(): void { ngOnDestroy(): void {
this.sub.unsubscribe(); this.accountSub.unsubscribe();
this.notificationSub.unsubscribe();
} }
onToogleAccountNotify(acc: AccountWrapper) { onToogleAccountNotify(acc: AccountWrapper) {
@ -102,3 +119,14 @@ export class LeftSideBarComponent implements OnInit, OnDestroy {
return false; return false;
} }
} }
export class AccountWithNotificationWrapper extends AccountWrapper {
// constructor(accountWrapper: AccountWrapper) {
// super();
// this.avatar = accountWrapper.avatar;
// this.info = accountWrapper.info;
// }
hasActivityNotifications: boolean;
}

View File

@ -2,12 +2,12 @@ import { Injectable } from '@angular/core';
import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http'; import { HttpHeaders, HttpClient, HttpResponse } from '@angular/common/http';
import { ApiRoutes } from './models/api.settings'; import { ApiRoutes } from './models/api.settings';
import { Account, Status, Results, Context, Relationship, Instance, Attachment } from "./models/mastodon.interfaces"; import { Account, Status, Results, Context, Relationship, Instance, Attachment, Notification } from "./models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state'; import { AccountInfo } from '../states/accounts.state';
import { StreamTypeEnum } from '../states/streams.state'; import { StreamTypeEnum } from '../states/streams.state';
@Injectable() @Injectable()
export class MastodonService { export class MastodonService {
private apiRoutes = new ApiRoutes(); private apiRoutes = new ApiRoutes();
constructor(private readonly httpClient: HttpClient) { } constructor(private readonly httpClient: HttpClient) { }
@ -229,6 +229,26 @@ export class MastodonService {
return this.httpClient.put<Attachment>(route, input, { headers: headers }).toPromise(); return this.httpClient.put<Attachment>(route, input, { headers: headers }).toPromise();
} }
getNotifications(account: AccountInfo, excludeTypes: string[] = null, maxId: string = null, sinceId: string = null, limit: number = 15): Promise<Notification[]> {
let route = `https://${account.instance}${this.apiRoutes.getNotifications}?limit=${limit}`;
if(maxId){
route += `&max_id=${maxId}`;
}
if(sinceId){
route += `&since_id=${sinceId}`;
}
if(excludeTypes && excludeTypes.length > 0) {
const excludeTypeArray = this.formatArray(excludeTypes, 'exclude_types');
route += `&${excludeTypeArray}`;
}
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
return this.httpClient.get<Notification[]>(route, { headers: headers }).toPromise();
}
private formatArray(data: string[], paramName: string): string { private formatArray(data: string[], paramName: string): string {
let result = ''; let result = '';
data.forEach(x => { data.forEach(x => {

View File

@ -0,0 +1,12 @@
import { TestBed } from '@angular/core/testing';
import { UserNotificationServiceService } from './user-notification-service.service';
xdescribe('UserNotificationServiceService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: UserNotificationServiceService = TestBed.get(UserNotificationServiceService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,94 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Subject, Observable, Subscription } from 'rxjs';
import { Store } from '@ngxs/store';
import { Status, Notification } from './models/mastodon.interfaces';
import { MastodonService } from './mastodon.service';
import { AccountInfo } from '../states/accounts.state';
import { NotificationService } from './notification.service';
@Injectable({
providedIn: 'root'
})
export class UserNotificationServiceService {
userNotifications = new BehaviorSubject<UserNotification[]>([]);
constructor(
private readonly notificationService: NotificationService,
private readonly mastodonService: MastodonService,
private readonly store: Store) {
this.fetchNotifications();
}
private fetchNotifications() {
let accounts = this.store.snapshot().registeredaccounts.accounts;
let promises: Promise<any>[] = [];
accounts.forEach(account => {
let getNotificationPromise = this.mastodonService.getNotifications(account)
.then((notifications: Notification[]) => {
this.processNotifications(account, notifications);
})
.catch(err => {
this.notificationService.notifyHttpError(err);
});
promises.push(getNotificationPromise);
});
Promise.all(promises)
.then(() => {
setTimeout(() => {
this.fetchNotifications();
}, 15 * 1000);
});
}
private processNotifications(account: AccountInfo, notifications: Notification[]) {
let currentNotifications = this.userNotifications.value;
const currentAccountNotifications = currentNotifications.find(x => x.account.id === account.id);
const userNotifications = notifications.filter(x => x.type !== 'mention');
const userMentions = notifications.filter(x => x.type === 'mention').map(x => x.status);
if (currentAccountNotifications) {
const currentUserNotifications = currentAccountNotifications.notifications;
const currentUserMentions = currentAccountNotifications.mentions;
const hasNewNotifications = (userNotifications.length === 0 && currentUserNotifications.length > 0)
|| (userNotifications.length > 0 && currentUserNotifications.length > 0) && (userNotifications[0].id !== currentUserNotifications[0].id);
const hasNewMentions = (userMentions.length === 0 && currentUserMentions.length > 0)
|| (userMentions.length > 0 && currentUserMentions.length > 0) && (userMentions[0].id !== currentUserMentions[0].id);
if (hasNewNotifications || hasNewMentions) {
currentAccountNotifications.hasNewMentions = hasNewMentions;
currentAccountNotifications.hasNewNotifications = hasNewNotifications;
currentAccountNotifications.notifications = userNotifications;
currentAccountNotifications.mentions = userMentions;
currentNotifications = currentNotifications.filter(x => x.account.id !== account.id);
currentNotifications.push(currentAccountNotifications);
this.userNotifications.next(currentNotifications);
}
} else {
const newNotifications = new UserNotification();
newNotifications.account = account;
newNotifications.hasNewNotifications = false; //TODO: check in local settings
newNotifications.hasNewMentions = false; //TODO: check in local settings
newNotifications.notifications = userNotifications;
newNotifications.mentions = userMentions;
currentNotifications.push(newNotifications);
this.userNotifications.next(currentNotifications);
}
}
}
export class UserNotification {
account: AccountInfo;
hasNewNotifications: boolean;
hasNewMentions: boolean;
notifications: Notification[] = [];
mentions: Status[] = [];
}