Merge pull request #22 from NicolasConstant/feature_open-user-profile
Feature open user profile
This commit is contained in:
commit
815213006d
@ -40,6 +40,7 @@ import { ThreadComponent } from './components/stream/thread/thread.component';
|
||||
import { HashtagComponent } from './components/stream/hashtag/hashtag.component';
|
||||
import { StreamOverlayComponent } from './components/stream/stream-overlay/stream-overlay.component';
|
||||
import { DatabindedTextComponent } from './components/stream/status/databinded-text/databinded-text.component';
|
||||
import { TimeAgoPipe } from './pipes/time-ago.pipe';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: "", redirectTo: "home", pathMatch: "full" },
|
||||
@ -72,7 +73,8 @@ const routes: Routes = [
|
||||
ThreadComponent,
|
||||
HashtagComponent,
|
||||
StreamOverlayComponent,
|
||||
DatabindedTextComponent
|
||||
DatabindedTextComponent,
|
||||
TimeAgoPipe
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -46,7 +46,7 @@ export class SearchComponent implements OnInit {
|
||||
//First candid implementation
|
||||
if (enabledAccounts.length > 0) {
|
||||
const candid_oneAccount = enabledAccounts[0];
|
||||
this.mastodonService.search(candid_oneAccount, data)
|
||||
this.mastodonService.search(candid_oneAccount, data, true)
|
||||
.then((results: Results) => {
|
||||
if (results) {
|
||||
console.warn(results);
|
||||
|
@ -1,15 +1,24 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hashtag',
|
||||
templateUrl: './hashtag.component.html',
|
||||
styleUrls: ['./hashtag.component.scss']
|
||||
selector: 'app-hashtag',
|
||||
templateUrl: './hashtag.component.html',
|
||||
styleUrls: ['./hashtag.component.scss']
|
||||
})
|
||||
export class HashtagComponent implements OnInit {
|
||||
hashtag: string;
|
||||
|
||||
constructor() { }
|
||||
@Output() browseAccount = new EventEmitter<string>();
|
||||
@Output() browseHashtag = new EventEmitter<string>();
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
@Input('currentHashtag')
|
||||
set currentAccount(hashtag: string) {
|
||||
this.hashtag = hashtag;
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
<div #content class="content" innerHTML="{{processedText}}" (click)="selectText()"></div>
|
||||
<div #content class="content" [class.selectable]="textIsSelectable" innerHTML="{{processedText}}" (click)="selectText()"></div>
|
@ -1,6 +1,6 @@
|
||||
@import "variables";
|
||||
|
||||
.content {
|
||||
.selectable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DatabindedTextComponent } from './databinded-text.component';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { isGeneratedFile } from '@angular/compiler/src/aot/util';
|
||||
|
||||
describe('DatabindedTextComponent', () => {
|
||||
let component: DatabindedTextComponent;
|
||||
@ -35,7 +36,7 @@ describe('DatabindedTextComponent', () => {
|
||||
const url = 'https://test.social/tags/programmers';
|
||||
const sample = `<p>bla1 <a href="${url}" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>${hashtag}</span></a> bla2</p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="programmers">#programmers</a>');
|
||||
expect(component.processedText).toContain('<a href class="hashtag-programmers">#programmers</a>');
|
||||
expect(component.processedText).toContain('bla1');
|
||||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
@ -59,6 +60,20 @@ describe('DatabindedTextComponent', () => {
|
||||
expect(component.processedText).toContain('bla2');
|
||||
});
|
||||
|
||||
it('should parse link - dual section', () => {
|
||||
const sample = `<p>Test.<br><a href="https://peertube.fr/videos/watch/69bb6e90-ec0f-49a3-9e28-41792f4a7c5f" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="ellipsis">peertube.fr/videos/watch/69bb6</span><span class="invisible">e90-ec0f-49a3-9e28-41792f4a7c5f</span></a></p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<p>Test.<br><a href class="link-httpspeertubefrvideoswatch69bb6e90ec0f49a39e2841792f4a7c5f" title="open link">peertube.fr/videos/watch/69bb6</a></p>');
|
||||
});
|
||||
|
||||
it('should parse link with special character', () => {
|
||||
const sample = `<p>Magnitude: 2.5 Depth: 3.4 km<br>Details: 2018/09/27 06:50:17 34.968N 120.685W<br>Location: 10 km (6 mi) W of Guadalupe, CA<br>Map: <a href="https://www.google.com/maps/place/34°58'4%20N+120°41'6%20W/@34.968,-120.685,10z" rel="noopener" target="_blank" class="status-link" title="https://www.google.com/maps/place/34%C2%B058'4%20N+120%C2%B041'6%20W/@34.968,-120.685,10z"><span class="invisible">https://www.</span><span class="ellipsis">google.com/maps/place/34°58'4%</span><span class="invisible">20N+120°41'6%20W/@34.968,-120.685,10z</span></a><br><a href="https://mastodon.cloud/tags/earthquake" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>EarthQuake</span></a> <a href="https://mastodon.cloud/tags/quake" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>Quake</span></a> <a href="https://mastodon.cloud/tags/california" class="mention hashtag status-link" rel="noopener" target="_blank">#<span>California</span></a></p>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="link-httpswwwgooglecommapsplace3458420N12041620W3496812068510z" title="open link">google.com/maps/place/34°58\'4%</a>');
|
||||
});
|
||||
|
||||
it('should parse combined hashtag, mention and link', () => {
|
||||
const hashtag = 'programmers';
|
||||
const hashtagUrl = 'https://test.social/tags/programmers';
|
||||
@ -67,7 +82,7 @@ describe('DatabindedTextComponent', () => {
|
||||
const linkUrl = 'mydomain.co/test';
|
||||
const sample = `<p>bla1 <a href="${hashtagUrl}" class="mention hashtag" rel="nofollow noopener" target="_blank">#<span>${hashtag}</span></a> bla2 <span class="h-card"><a href="${mentionUrl}" class="u-url mention" rel="nofollow noopener" target="_blank">@<span>${mention}</span></a></span> bla3 <a href="https://${linkUrl}" rel="nofollow noopener" target="_blank"><span class="invisible">https://</span><span class="">${linkUrl}</span><span class="invisible"></span></a> bla4</p>`;
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="programmers">#programmers</a>');
|
||||
expect(component.processedText).toContain('<a href class="hashtag-programmers">#programmers</a>');
|
||||
expect(component.processedText).toContain('<a href class="account--sengi_app-mastodon-social" title="@sengi_app@mastodon.social">@sengi_app</a>');
|
||||
expect(component.processedText).toContain('<a href class="link-httpsmydomaincotest" title="open link">mydomain.co/test</a>');
|
||||
expect(component.processedText).toContain('bla1');
|
||||
@ -75,4 +90,27 @@ describe('DatabindedTextComponent', () => {
|
||||
expect(component.processedText).toContain('bla3');
|
||||
expect(component.processedText).toContain('bla4');
|
||||
});
|
||||
|
||||
it('should parse link - GNU social in Mastodon', () => {
|
||||
const sample = `bla1 <a href="https://www.lemonde.fr/planete.html?xtor=RSS-3208" rel="nofollow noopener" class="" target="_blank">https://social.bitcast.info/url/819438</a>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="link-httpswwwlemondefrplanetehtmlxtorRSS3208" title="open link">https://social.bitcast.info/url/819438</a>');
|
||||
expect(component.processedText).toContain('bla1');
|
||||
});
|
||||
|
||||
it('should parse mention - Pleroma in Mastodon', () => {
|
||||
const sample = `<div>bla1 <br> @<a href="https://instance.club/user/1" class="h-card mention status-link" rel="noopener" target="_blank" title="https://instance.club/user/1">user</a> </div>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<a href class="account--user-instance-club" title="@user@instance.club">@user</a>');
|
||||
expect(component.processedText).toContain('bla1');
|
||||
})
|
||||
|
||||
it('should parse mention - Pleroma in Mastodon - 2', () => {
|
||||
const sample = `<div><span><a class="mention status-link" href="https://pleroma.site/users/kaniini" rel="noopener" target="_blank" title="kaniini@pleroma.site">@<span>kaniini</span></a></span> <span><a class="mention status-link" href="https://mastodon.social/@Gargron" rel="noopener" target="_blank" title="Gargron@mastodon.social">@<span>Gargron</span></a></span> bla1?</div>`;
|
||||
|
||||
component.text = sample;
|
||||
expect(component.processedText).toContain('<div><span><a href class="account--kaniini-pleroma-site" title="@kaniini@pleroma.site">@kaniini</a> <span><a href class="account--Gargron-mastodon-social" title="@Gargron@mastodon.social">@Gargron</a> bla1?</div>');
|
||||
});
|
||||
});
|
||||
|
@ -18,30 +18,44 @@ export class DatabindedTextComponent implements OnInit {
|
||||
@Output() hashtagSelected = new EventEmitter<string>();
|
||||
@Output() textSelected = new EventEmitter();
|
||||
|
||||
@Input() textIsSelectable: boolean = true;
|
||||
|
||||
@Input('text')
|
||||
set text(value: string) {
|
||||
this.processedText = '';
|
||||
|
||||
let linksSections = value.split('<a ');
|
||||
|
||||
for (let section of linksSections) {
|
||||
console.log(section);
|
||||
|
||||
if (!section.includes('href')) {
|
||||
this.processedText += section;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (section.includes('class="mention hashtag"') || section.includes('target="_blank">#')) {
|
||||
console.log('process hashtag');
|
||||
this.processHashtag(section);
|
||||
} else if (section.includes('class="u-url mention"') || section.includes('class="mention"')) {
|
||||
console.log('process mention');
|
||||
this.processUser(section);
|
||||
try {
|
||||
this.processHashtag(section);
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('process hashtag');
|
||||
console.warn(value);
|
||||
}
|
||||
|
||||
} else if (section.includes('class="u-url mention"') || section.includes('class="mention"') || section.includes('class="mention status-link"') || section.includes('class="h-card mention')) {
|
||||
try {
|
||||
this.processUser(section);
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('process mention');
|
||||
console.warn(value);
|
||||
}
|
||||
} else {
|
||||
console.log('process link');
|
||||
console.log(section);
|
||||
this.processLink(section);
|
||||
try {
|
||||
this.processLink(section);
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('process link');
|
||||
console.warn(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,27 +64,41 @@ export class DatabindedTextComponent implements OnInit {
|
||||
let extractedLinkAndNext = section.split('</a>');
|
||||
let extractedHashtag = extractedLinkAndNext[0].split('#')[1].replace('<span>', '').replace('</span>', '');
|
||||
|
||||
this.processedText += ` <a href class="${extractedHashtag}">#${extractedHashtag}</a>`;
|
||||
let classname = this.getClassNameForHastag(extractedHashtag);
|
||||
this.processedText += ` <a href class="${classname}">#${extractedHashtag}</a>`;
|
||||
if (extractedLinkAndNext[1]) this.processedText += extractedLinkAndNext[1];
|
||||
this.hashtags.push(extractedHashtag);
|
||||
}
|
||||
|
||||
private processUser(section: string) {
|
||||
let mentionClass = 'class="mention"';
|
||||
if (section.includes('class="u-url mention"'))
|
||||
mentionClass = 'class="u-url mention"';
|
||||
let extractedAccountAndNext: string[];
|
||||
let extractedAccountName: string;
|
||||
|
||||
let extractedAccountAndNext = section.split('</a></span>');
|
||||
|
||||
let extractedAccountName = extractedAccountAndNext[0].split('@<span>')[1].replace('<span>', '').replace('</span>', '');
|
||||
if (!section.includes('@<span>')) { //GNU social
|
||||
extractedAccountAndNext = section.split('</a>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('>')[1];
|
||||
} else {
|
||||
extractedAccountAndNext = section.split('</a></span>');
|
||||
extractedAccountName = extractedAccountAndNext[0].split('@<span>')[1].replace('<span>', '').replace('</span>', '');
|
||||
}
|
||||
|
||||
let extractedAccountLink = extractedAccountAndNext[0].split('href="https://')[1].split('"')[0].replace(' ', '').replace('@', '').split('/');
|
||||
let extractedAccount = `@${extractedAccountLink[1]}@${extractedAccountLink[0]}`;
|
||||
|
||||
let domain = extractedAccountLink[0];
|
||||
//let username = extractedAccountLink[extractedAccountLink.length - 1];
|
||||
|
||||
let extractedAccount = `@${extractedAccountName}@${domain}`;
|
||||
|
||||
let classname = this.getClassNameForAccount(extractedAccount);
|
||||
this.processedText += ` <a href class="${classname}" title="${extractedAccount}">@${extractedAccountName}</a>`;
|
||||
this.processedText += `<a href class="${classname}" title="${extractedAccount}">@${extractedAccountName}</a>`;
|
||||
|
||||
if (extractedAccountAndNext[1])
|
||||
this.processedText += extractedAccountAndNext[1];
|
||||
|
||||
//GNU Social clean up
|
||||
if(this.processedText.includes('@<a'))
|
||||
this.processedText = this.processedText.replace('@<a', '<a');
|
||||
|
||||
if (extractedAccountAndNext[1]) this.processedText += extractedAccountAndNext[1];
|
||||
this.accounts.push(extractedAccount);
|
||||
}
|
||||
|
||||
@ -78,20 +106,17 @@ export class DatabindedTextComponent implements OnInit {
|
||||
let extractedLinkAndNext = section.split('</a>')
|
||||
let extractedUrl = extractedLinkAndNext[0].split('"')[1];
|
||||
|
||||
console.warn(extractedLinkAndNext[0]);
|
||||
console.warn(extractedLinkAndNext[0].split('<span class="ellipsis">'));
|
||||
|
||||
let extractedName = '';
|
||||
try {
|
||||
extractedName = extractedLinkAndNext[0].split('<span class="ellipsis">')[1].split('</span>')[0];
|
||||
} catch (err){
|
||||
} catch (err) {
|
||||
try {
|
||||
extractedName = extractedLinkAndNext[0].split('<span class="invisible">https://</span><span class="">')[1].split('</span>')[0];
|
||||
extractedName = extractedLinkAndNext[0].split('<span class="invisible">https://</span><span class="">')[1].split('</span>')[0];
|
||||
}
|
||||
catch(err){
|
||||
extractedName = extractedLinkAndNext[0].split('rel="nofollow noopener" target="_blank">')[1].split('</span>')[0];
|
||||
catch (err) {
|
||||
extractedName = extractedLinkAndNext[0].split(' target="_blank">')[1].split('</span>')[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.links.push(extractedUrl);
|
||||
let classname = this.getClassNameForLink(extractedUrl);
|
||||
@ -109,7 +134,8 @@ export class DatabindedTextComponent implements OnInit {
|
||||
|
||||
ngAfterViewInit() {
|
||||
for (const hashtag of this.hashtags) {
|
||||
let el = this.contentElement.nativeElement.querySelector(`.${hashtag}`);
|
||||
let classname = this.getClassNameForHastag(hashtag);
|
||||
let el = this.contentElement.nativeElement.querySelector(`.${classname}`);
|
||||
|
||||
this.renderer.listen(el, 'click', (event) => {
|
||||
event.preventDefault();
|
||||
@ -146,6 +172,11 @@ export class DatabindedTextComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private getClassNameForHastag(value: string): string {
|
||||
let res = value.replace(/[.,\/#?!@$%+\^&\*;:{}=\-_`~()]/g, "");
|
||||
return `hashtag-${res}`;
|
||||
}
|
||||
|
||||
private getClassNameForAccount(value: string): string {
|
||||
let res = value;
|
||||
while (res.includes('.')) res = res.replace('.', '-');
|
||||
@ -154,22 +185,19 @@ export class DatabindedTextComponent implements OnInit {
|
||||
}
|
||||
|
||||
private getClassNameForLink(value: string): string {
|
||||
let res = value.replace(/[.,\/#?!@$%\^&\*;:{}=\-_`~()]/g, "");
|
||||
let res = value.replace(/[.,\/#?!@°$%+\'\^&\*;:{}=\-_`~()]/g, "");
|
||||
return `link-${res}`;
|
||||
}
|
||||
|
||||
private selectAccount(account: string) {
|
||||
console.warn(`select ${account}`);
|
||||
this.accountSelected.next(account);
|
||||
}
|
||||
|
||||
private selectHashtag(hashtag: string) {
|
||||
console.warn(`select ${hashtag}`);
|
||||
this.hashtagSelected.next(hashtag);
|
||||
}
|
||||
|
||||
selectText() {
|
||||
console.warn(`selectText`);
|
||||
this.textSelected.next();
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { Component, OnInit, Input, Output, EventEmitter, ElementRef, ViewChild } from '@angular/core';
|
||||
import { Store } from '@ngxs/store';
|
||||
// import { Store } from '@ngxs/store';
|
||||
import { MastodonService, VisibilityEnum } from '../../../../services/mastodon.service';
|
||||
import { AccountInfo } from '../../../../states/accounts.state';
|
||||
// import { AccountInfo } from '../../../../states/accounts.state';
|
||||
import { StatusWrapper } from '../../stream.component';
|
||||
import { Status } from '../../../../services/models/mastodon.interfaces';
|
||||
import { ToolsService } from '../../../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-reply-to-status',
|
||||
@ -22,7 +23,8 @@ export class ReplyToStatusComponent implements OnInit {
|
||||
privacyList: string[] = ['Public', 'Unlisted', 'Follows-only', 'DM'];
|
||||
|
||||
constructor(
|
||||
private readonly store: Store,
|
||||
// private readonly store: Store,
|
||||
private readonly toolsService: ToolsService,
|
||||
private readonly mastodonService: MastodonService) { }
|
||||
|
||||
ngOnInit() {
|
||||
@ -39,8 +41,7 @@ export class ReplyToStatusComponent implements OnInit {
|
||||
}
|
||||
|
||||
onSubmit(): boolean {
|
||||
const accounts = this.getRegisteredAccounts();
|
||||
const selectedAccounts = accounts.filter(x => x.isSelected);
|
||||
const selectedAccounts = this.toolsService.getSelectedAccounts();
|
||||
|
||||
let visibility: VisibilityEnum = VisibilityEnum.Unknown;
|
||||
switch (this.selectedPrivacy) {
|
||||
@ -72,10 +73,10 @@ export class ReplyToStatusComponent implements OnInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
private getRegisteredAccounts(): AccountInfo[] {
|
||||
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
return regAccounts;
|
||||
}
|
||||
// private getRegisteredAccounts(): AccountInfo[] {
|
||||
// var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
// return regAccounts;
|
||||
// }
|
||||
|
||||
onCtrlEnter(): boolean {
|
||||
this.onSubmit();
|
||||
|
@ -13,10 +13,10 @@
|
||||
</span>
|
||||
</a>
|
||||
<div class="status__created-at" title="{{ displayedStatus.created_at | date: 'full' }}">{{
|
||||
getCompactRelativeTime(status.created_at) }}</div>
|
||||
status.created_at | timeAgo | async }}</div>
|
||||
<!-- <div #content class="status__content" innerHTML="{{displayedStatus.content}}"></div> -->
|
||||
|
||||
<app-databinded-text class="status__content" [text]="displayedStatus.content"></app-databinded-text>
|
||||
<app-databinded-text class="status__content" [text]="displayedStatus.content" (accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)" (textSelected)="textSelected()"></app-databinded-text>
|
||||
|
||||
|
||||
<app-attachements *ngIf="hasAttachments" class="attachments" [attachments]="displayedStatus.media_attachments"></app-attachements>
|
||||
|
@ -17,12 +17,10 @@ export class StatusComponent implements OnInit {
|
||||
hasAttachments: boolean;
|
||||
replyingToStatus: boolean;
|
||||
|
||||
@Output() browseAccount = new EventEmitter<Account>();
|
||||
@Output() browseAccount = new EventEmitter<string>();
|
||||
@Output() browseHashtag = new EventEmitter<string>();
|
||||
@Output() browseThread = new EventEmitter<string>();
|
||||
|
||||
|
||||
|
||||
private _statusWrapper: StatusWrapper;
|
||||
status: Status;
|
||||
@Input('statusWrapper')
|
||||
@ -74,27 +72,31 @@ export class StatusComponent implements OnInit {
|
||||
// }
|
||||
|
||||
openAccount(account: Account): boolean {
|
||||
this.browseAccount.next(account);
|
||||
let accountName = account.acct;
|
||||
if(!accountName.includes('@'))
|
||||
accountName += `@${account.url.replace('https://', '').split('/')[0]}`;
|
||||
|
||||
this.browseAccount.next(accountName);
|
||||
return false;
|
||||
}
|
||||
|
||||
getCompactRelativeTime(d: string): string {
|
||||
const date = (new Date(d)).getTime();
|
||||
const now = Date.now();
|
||||
const timeDelta = (now - date) / (1000);
|
||||
// getCompactRelativeTime(d: string): string {
|
||||
// const date = (new Date(d)).getTime();
|
||||
// const now = Date.now();
|
||||
// const timeDelta = (now - date) / (1000);
|
||||
|
||||
if (timeDelta < 60) {
|
||||
return `${timeDelta | 0}s`;
|
||||
} else if (timeDelta < 60 * 60) {
|
||||
return `${timeDelta / 60 | 0}m`;
|
||||
} else if (timeDelta < 60 * 60 * 24) {
|
||||
return `${timeDelta / (60 * 60) | 0}h`;
|
||||
} else if (timeDelta < 60 * 60 * 24 * 31) {
|
||||
return `${timeDelta / (60 * 60 * 24) | 0}d`;
|
||||
}
|
||||
// if (timeDelta < 60) {
|
||||
// return `${timeDelta | 0}s`;
|
||||
// } else if (timeDelta < 60 * 60) {
|
||||
// return `${timeDelta / 60 | 0}m`;
|
||||
// } else if (timeDelta < 60 * 60 * 24) {
|
||||
// return `${timeDelta / (60 * 60) | 0}h`;
|
||||
// } else if (timeDelta < 60 * 60 * 24 * 31) {
|
||||
// return `${timeDelta / (60 * 60 * 24) | 0}d`;
|
||||
// }
|
||||
|
||||
return formatDate(date, 'MM/dd', this.locale);
|
||||
}
|
||||
// return formatDate(date, 'MM/dd', this.locale);
|
||||
// }
|
||||
|
||||
openReply(): boolean {
|
||||
this.replyingToStatus = !this.replyingToStatus;
|
||||
@ -107,8 +109,22 @@ export class StatusComponent implements OnInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
test(): boolean {
|
||||
console.warn('heeeeyaaa!');
|
||||
return false;
|
||||
// test(): boolean {
|
||||
// console.warn('heeeeyaaa!');
|
||||
// return false;
|
||||
// }
|
||||
|
||||
accountSelected(accountName: string): void {
|
||||
console.warn(`status comp: accountSelected ${accountName}`);
|
||||
this.browseAccount.next(accountName);
|
||||
}
|
||||
|
||||
hashtagSelected(hashtag: string): void {
|
||||
console.warn(`status comp: hashtagSelected ${hashtag}`);
|
||||
this.browseHashtag.next(hashtag);
|
||||
}
|
||||
|
||||
textSelected(): void {
|
||||
console.warn(`status comp: textSelected`);
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
<div class="stream-overlay">
|
||||
<div class="stream-overlay__header">
|
||||
<a href class="overlay-close" (click)="close()">CLOSE</a>
|
||||
<a href class="overlay-previous">PREV</a>
|
||||
<a href class="overlay-next">NEXT</a>
|
||||
<a href class="overlay-close" (click)="close()">CLOSE</a>
|
||||
<a href class="overlay-previous" (click)="previous()">PREV</a>
|
||||
<a href class="overlay-refresh" *ngIf="canRefresh" (click)="refresh()">REFRESH</a>
|
||||
<a href class="overlay-next" *ngIf="canGoForward" (click)="next()">NEXT</a>
|
||||
</div>
|
||||
<!-- <div class="stream-overlay__title">
|
||||
Account
|
||||
</div> -->
|
||||
<app-user-profile *ngIf="browseAccount" [currentAccount]="browseAccount"></app-user-profile>
|
||||
|
||||
<app-user-profile *ngIf="accountName" [currentAccount]="accountName" (browseAccount)="accountSelected($event)" (browseHashtag)="hashtagSelected($event)"></app-user-profile>
|
||||
<app-hashtag *ngIf="browseHashtag"></app-hashtag>
|
||||
<app-thread *ngIf="browseThread"></app-thread>
|
||||
</div>
|
@ -1,14 +1,11 @@
|
||||
@import "variables";
|
||||
@import "commons";
|
||||
.stream-overlay {
|
||||
// position: absolute;
|
||||
|
||||
// z-index: 50;
|
||||
width: $stream-column-width;
|
||||
height: calc(100%);
|
||||
background-color: $column-color;
|
||||
|
||||
|
||||
// margin: 0 0 0 $stream-column-separator;
|
||||
background-color: $column-color; // margin: 0 0 0 $stream-column-separator;
|
||||
// outline: 1px red solid;
|
||||
// float: left;
|
||||
&__header {
|
||||
@ -16,14 +13,12 @@
|
||||
height: 30px;
|
||||
background-color: $column-header-background-color;
|
||||
padding: 6px 10px 0 10px;
|
||||
& a {
|
||||
& a {
|
||||
color: whitesmoke;
|
||||
font-size: 0.8em;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
font-weight: normal; // margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
width: calc(100%);
|
||||
height: 30px;
|
||||
@ -34,18 +29,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-previous {
|
||||
display: block;
|
||||
float: left;
|
||||
.overlay {
|
||||
margin: 0;
|
||||
&-previous {
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
&-refresh {
|
||||
display: block;
|
||||
float: left;
|
||||
margin-left: 65px;
|
||||
}
|
||||
&-next {
|
||||
display: block;
|
||||
float: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
&-close {
|
||||
display: block;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay-next {
|
||||
display: block;
|
||||
float: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.overlay-close {
|
||||
display: block;
|
||||
float: right;
|
||||
.not-active {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
text-decoration: none;
|
||||
color: gray !important;
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core';
|
||||
import { Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { Account, Results } from "../../../services/models/mastodon.interfaces";
|
||||
import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stream-overlay',
|
||||
@ -7,34 +9,34 @@ import { Account } from "../../../services/models/mastodon.interfaces";
|
||||
styleUrls: ['./stream-overlay.component.scss']
|
||||
})
|
||||
export class StreamOverlayComponent implements OnInit {
|
||||
private account: Account;
|
||||
private thread: string;
|
||||
private hashtag: string;
|
||||
private previousElements: OverlayBrowsing[] = [];
|
||||
private nextElements: OverlayBrowsing[] = [];
|
||||
private currentElement: OverlayBrowsing;
|
||||
|
||||
canRefresh: boolean;
|
||||
canGoForward: boolean;
|
||||
|
||||
accountName: string;
|
||||
thread: string;
|
||||
hashtag: string;
|
||||
|
||||
@Output() closeOverlay = new EventEmitter();
|
||||
|
||||
@Input('browseAccount')
|
||||
set browseAccount(account: Account) {
|
||||
this.account = account;
|
||||
}
|
||||
get browseAccount(): Account{
|
||||
return this.account;
|
||||
@Input('browseAccount')
|
||||
set browseAccount(accountName: string) {
|
||||
this.accountSelected(accountName);
|
||||
// this.accountName = accountName;
|
||||
}
|
||||
|
||||
@Input('browseThread')
|
||||
@Input('browseThread')
|
||||
set browseThread(thread: string) {
|
||||
this.thread = thread;
|
||||
}
|
||||
get browseThread(): string{
|
||||
return this.thread;
|
||||
// this.thread = thread;
|
||||
}
|
||||
|
||||
@Input('browseHashtag')
|
||||
@Input('browseHashtag')
|
||||
set browseHashtag(hashtag: string) {
|
||||
this.hashtag = hashtag;
|
||||
}
|
||||
get browseHashtag(): string{
|
||||
return this.hashtag;
|
||||
this.hashtagSelected(hashtag);
|
||||
// this.hashtag = hashtag;
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
@ -47,4 +49,107 @@ export class StreamOverlayComponent implements OnInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
next(): boolean {
|
||||
console.log('next');
|
||||
|
||||
if (this.nextElements.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.currentElement) {
|
||||
this.previousElements.push(this.currentElement);
|
||||
}
|
||||
|
||||
const nextElement = this.nextElements.pop();
|
||||
this.loadElement(nextElement);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
previous(): boolean {
|
||||
console.log('previous');
|
||||
|
||||
if (this.previousElements.length === 0) {
|
||||
this.closeOverlay.next();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.currentElement) {
|
||||
this.nextElements.push(this.currentElement);
|
||||
}
|
||||
|
||||
const previousElement = this.previousElements.pop();
|
||||
this.loadElement(previousElement);
|
||||
|
||||
this.canGoForward = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
refresh(): boolean {
|
||||
console.log('refresh');
|
||||
return false;
|
||||
}
|
||||
|
||||
accountSelected(accountName: string): void {
|
||||
if(!accountName) return;
|
||||
|
||||
console.log('accountSelected');
|
||||
this.nextElements.length = 0;
|
||||
if (this.currentElement) {
|
||||
this.previousElements.push(this.currentElement);
|
||||
}
|
||||
const newElement = new OverlayBrowsing(null, accountName, null);
|
||||
this.loadElement(newElement);
|
||||
this.canGoForward = false;
|
||||
}
|
||||
|
||||
hashtagSelected(hashtag: string): void {
|
||||
if(!hashtag) return;
|
||||
|
||||
console.log('hashtagSelected');
|
||||
this.nextElements.length = 0;
|
||||
if (this.currentElement) {
|
||||
this.previousElements.push(this.currentElement);
|
||||
}
|
||||
const newElement = new OverlayBrowsing(hashtag, null, null);
|
||||
this.loadElement(newElement);
|
||||
this.canGoForward = false;
|
||||
}
|
||||
|
||||
private loadElement(element: OverlayBrowsing) {
|
||||
this.currentElement = element;
|
||||
|
||||
this.accountName = this.currentElement.account;
|
||||
this.hashtag = this.currentElement.hashtag;
|
||||
this.thread = this.currentElement.thread;
|
||||
}
|
||||
}
|
||||
|
||||
class OverlayBrowsing {
|
||||
constructor(
|
||||
public readonly hashtag: string,
|
||||
public readonly account: string,
|
||||
public readonly thread: string) {
|
||||
|
||||
console.warn(`OverlayBrowsing: ${hashtag} ${account} ${thread}`);
|
||||
|
||||
if (hashtag) {
|
||||
this.type = OverlayEnum.hashtag;
|
||||
} else if (account) {
|
||||
this.type = OverlayEnum.account;
|
||||
} else if (thread) {
|
||||
this.type = OverlayEnum.thread;
|
||||
} else {
|
||||
throw Error('NotImplemented');
|
||||
}
|
||||
}
|
||||
|
||||
type: OverlayEnum;
|
||||
}
|
||||
|
||||
enum OverlayEnum {
|
||||
unknown = 0,
|
||||
hashtag = 1,
|
||||
account = 2,
|
||||
thread = 3
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
<div class="stream-column">
|
||||
|
||||
<app-stream-overlay class="stream-overlay" *ngIf="overlayActive"
|
||||
(closeOverlay)="closeOverlay()" [browseAccount]="overlayAccountToBrowse"></app-stream-overlay>
|
||||
(closeOverlay)="closeOverlay()"
|
||||
[browseAccount]="overlayAccountToBrowse"
|
||||
[browseHashtag]="overlayHashtagToBrowse"></app-stream-overlay>
|
||||
|
||||
<div class="stream-column__stream-header">
|
||||
<a href title="return to top" (click)="goToTop()">
|
||||
@ -11,7 +13,7 @@
|
||||
<div class="stream-toots flexcroll" #statusstream (scroll)="onScroll()">
|
||||
<!-- data-simplebar -->
|
||||
<div class="stream-toots__status" *ngFor="let statusWrapper of statuses">
|
||||
<app-status [statusWrapper]="statusWrapper" (browseAccount)="browseAccount($event)"></app-status>
|
||||
<app-status [statusWrapper]="statusWrapper" (browseAccount)="browseAccount($event)" (browseHashtag)="browseHashtag($event)"></app-status>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,4 +1,5 @@
|
||||
@import "variables";
|
||||
@import "commons";
|
||||
.stream-column {
|
||||
position: relative;
|
||||
|
||||
@ -31,25 +32,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
.flexcroll {
|
||||
scrollbar-face-color: #08090d;
|
||||
scrollbar-shadow-color: #08090d;
|
||||
scrollbar-highlight-color: #08090d;
|
||||
scrollbar-3dlight-color: #08090d;
|
||||
scrollbar-darkshadow-color: #08090d;
|
||||
scrollbar-track-color: #08090d;
|
||||
scrollbar-arrow-color: #08090d;
|
||||
&::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 0px;
|
||||
border-radius: 0px;
|
||||
// background: #08090d;
|
||||
background: lighten($color-primary, 5);
|
||||
// -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
// .flexcroll {
|
||||
// scrollbar-face-color: #08090d;
|
||||
// scrollbar-shadow-color: #08090d;
|
||||
// scrollbar-highlight-color: #08090d;
|
||||
// scrollbar-3dlight-color: #08090d;
|
||||
// scrollbar-darkshadow-color: #08090d;
|
||||
// scrollbar-track-color: #08090d;
|
||||
// scrollbar-arrow-color: #08090d;
|
||||
// &::-webkit-scrollbar {
|
||||
// width: 7px;
|
||||
// }
|
||||
// &::-webkit-scrollbar-thumb {
|
||||
// -webkit-border-radius: 0px;
|
||||
// border-radius: 0px;
|
||||
// // background: #08090d;
|
||||
// background: lighten($color-primary, 5);
|
||||
// // -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5);
|
||||
// }
|
||||
// }
|
||||
|
||||
.stream-overlay {
|
||||
position: absolute;
|
||||
|
@ -22,7 +22,8 @@ export class StreamComponent implements OnInit {
|
||||
private bufferWasCleared: boolean;
|
||||
|
||||
overlayActive: boolean;
|
||||
overlayAccountToBrowse: Account;
|
||||
overlayAccountToBrowse: string;
|
||||
overlayHashtagToBrowse: string;
|
||||
|
||||
@Input()
|
||||
set streamElement(streamElement: StreamElement) {
|
||||
@ -50,17 +51,20 @@ export class StreamComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
browseAccount(account: Account): void {
|
||||
browseAccount(account: string): void {
|
||||
this.overlayAccountToBrowse = account;
|
||||
this.overlayActive = true;
|
||||
this.overlayHashtagToBrowse = null;
|
||||
this.overlayActive = true;
|
||||
}
|
||||
|
||||
browseHashtag(hashtag: any): void {
|
||||
console.warn('browseHashtag');
|
||||
console.warn(hashtag);
|
||||
browseHashtag(hashtag: string): void {
|
||||
console.warn(`browseHashtag ${hashtag}`);
|
||||
this.overlayAccountToBrowse = null;
|
||||
this.overlayHashtagToBrowse = hashtag;
|
||||
this.overlayActive = true;
|
||||
}
|
||||
|
||||
browseThread(thread: any): void {
|
||||
browseThread(thread: string): void {
|
||||
console.warn('browseThread');
|
||||
console.warn(thread);
|
||||
}
|
||||
|
@ -1,9 +1,28 @@
|
||||
<div class="profile-header">
|
||||
<img class="profile-header__header" src="{{account.header}}" alt="header" />
|
||||
<img class="profile-header__avatar" src="{{account.avatar}}" alt="header" />
|
||||
<h2 class="profile-header__display-name">{{account.display_name}}</h2>
|
||||
<h2 class="profile-header__fullhandle">@{{account.acct}}</h2>
|
||||
</div>
|
||||
<div class="profile-description" *ngIf="hasNote">
|
||||
<p innerHTML="{{account.note}}"></p>
|
||||
<div class="profile flexcroll">
|
||||
<app-waiting-animation *ngIf="isLoading" class="waiting-icon"></app-waiting-animation>
|
||||
|
||||
<div *ngIf="account" class="profile-header" [ngStyle]="{'background-image':'url('+account.header+')'}">
|
||||
<div class="profile-header__inner">
|
||||
<!-- <img class="profile-header__header" src="{{account.header}}" alt="header" /> -->
|
||||
<img class="profile-header__avatar" src="{{account.avatar}}" alt="header" />
|
||||
<h2 class="profile-header__display-name">{{account.display_name}}</h2>
|
||||
<h2 class="profile-header__fullhandle"><a href="{{account.url}}" target="_blank">@{{account.acct}}</a></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="account && hasNote" class="profile-description">
|
||||
<app-databinded-text class="status__content" [textIsSelectable]="false" [text]="account.note" (accountSelected)="accountSelected($event)"
|
||||
(hashtagSelected)="hashtagSelected($event)"></app-databinded-text>
|
||||
<!-- <p innerHTML="{{account.note}}"></p> -->
|
||||
</div>
|
||||
<div class="profile-statuses">
|
||||
<app-waiting-animation *ngIf="statusLoading" class="waiting-icon"></app-waiting-animation>
|
||||
|
||||
<div *ngIf="!isLoading && !statusLoading && statuses.length == 0" class="profile-no-toots">
|
||||
no toots found
|
||||
</div>
|
||||
|
||||
<div *ngFor="let statusWrapper of statuses">
|
||||
<app-status [statusWrapper]="statusWrapper" (browseAccount)="accountSelected($event)"></app-status>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,48 +1,52 @@
|
||||
@import "variables";
|
||||
.profile-header {
|
||||
position: relative;
|
||||
height: 140px;
|
||||
overflow: hidden; // background-color: black;
|
||||
border-bottom: 1px solid black;
|
||||
& h2 {
|
||||
font-size: $default-font-size;
|
||||
}
|
||||
&__header {
|
||||
position: absolute;
|
||||
// width: calc(100%);
|
||||
@import "commons";
|
||||
.profile {
|
||||
overflow: auto;
|
||||
height: calc(100% - 30px);
|
||||
|
||||
width: calc(100%);
|
||||
height: auto;
|
||||
&-header {
|
||||
background-size: cover;
|
||||
position: relative; // height: 140px;
|
||||
overflow: hidden; // background-color: black;
|
||||
border-bottom: 1px solid black;
|
||||
& h2 {
|
||||
font-size: $default-font-size;
|
||||
}
|
||||
&__inner {
|
||||
overflow: auto;
|
||||
height: 160px;
|
||||
background-color: rgba(0, 0, 0, .45);
|
||||
}
|
||||
&__avatar {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
width: 80px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
&__display-name {
|
||||
position: absolute;
|
||||
top: 105px;
|
||||
left: 15px;
|
||||
color: white;
|
||||
}
|
||||
&__fullhandle a {
|
||||
position: absolute;
|
||||
top: 130px;
|
||||
left: 15px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
float: left;
|
||||
display: block;
|
||||
opacity: 0.3;
|
||||
&-description {
|
||||
padding: 10px 10px 15px 10px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
&__avatar {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
width: 80px;
|
||||
border-radius: 50%; // border: 1px solid black;
|
||||
// background-color: black;
|
||||
}
|
||||
&__display-name {
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
left: 115px;
|
||||
// font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
&__fullhandle {
|
||||
position: absolute;
|
||||
top: 105px;
|
||||
left: 15px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-description {
|
||||
padding: 5px 10px 0 10px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid black;
|
||||
&-no-toots {
|
||||
text-align: center;
|
||||
margin: 15px;
|
||||
border: 2px solid whitesmoke;
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Account } from "../../../services/models/mastodon.interfaces";
|
||||
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
|
||||
import { Account, Status, Results } from "../../../services/models/mastodon.interfaces";
|
||||
import { MastodonService } from '../../../services/mastodon.service';
|
||||
import { ToolsService } from '../../../services/tools.service';
|
||||
import { StatusWrapper } from '../stream.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-profile',
|
||||
@ -10,17 +13,95 @@ export class UserProfileComponent implements OnInit {
|
||||
account: Account;
|
||||
hasNote: boolean;
|
||||
|
||||
isLoading: boolean;
|
||||
statusLoading: boolean;
|
||||
error: string;
|
||||
|
||||
statuses: StatusWrapper[] = [];
|
||||
|
||||
private accountName: string;
|
||||
|
||||
@Output() browseAccount = new EventEmitter<string>();
|
||||
@Output() browseHashtag = new EventEmitter<string>();
|
||||
|
||||
@Input('currentAccount')
|
||||
set currentAccount(account: Account) {
|
||||
this.account = account;
|
||||
this.hasNote = account && account.note && account.note !== '<p></p>';
|
||||
console.warn('currentAccount');
|
||||
console.warn(account);
|
||||
//set currentAccount(account: Account) {
|
||||
set currentAccount(accountName: string) {
|
||||
this.statuses.length = 0;
|
||||
this.isLoading = true;
|
||||
|
||||
this.loadAccount(accountName)
|
||||
.then((account: Account) => {
|
||||
this.account = account;
|
||||
return this.getStatuses(this.account);
|
||||
})
|
||||
.catch(err => {
|
||||
this.error = 'Error when retrieving account';
|
||||
this.isLoading = false;
|
||||
this.statusLoading = false;
|
||||
console.warn(this.error);
|
||||
});
|
||||
|
||||
// this.account = account;
|
||||
// this.hasNote = account && account.note && account.note !== '<p></p>';
|
||||
// console.warn('currentAccount');
|
||||
// console.warn(account);
|
||||
// this.getStatuses(account);
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
constructor(
|
||||
private readonly mastodonService: MastodonService,
|
||||
private readonly toolsService: ToolsService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
accountSelected(accountName: string): void {
|
||||
this.browseAccount.next(accountName);
|
||||
}
|
||||
|
||||
hashtagSelected(hashtag: string): void {
|
||||
this.browseHashtag.next(hashtag);
|
||||
}
|
||||
|
||||
private loadAccount(accountName: string): Promise<Account> {
|
||||
this.account = null;
|
||||
this.accountName = accountName;
|
||||
let selectedAccounts = this.toolsService.getSelectedAccounts();
|
||||
|
||||
if (selectedAccounts.length === 0) {
|
||||
this.error = 'no user selected';
|
||||
console.error(this.error);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
return this.mastodonService.search(selectedAccounts[0], accountName, true)
|
||||
.then((result: Results) => {
|
||||
console.warn(result);
|
||||
this.isLoading = false;
|
||||
return result.accounts[0];
|
||||
});
|
||||
}
|
||||
|
||||
private getStatuses(account: Account): Promise<void> {
|
||||
let selectedAccounts = this.toolsService.getSelectedAccounts();
|
||||
if (selectedAccounts.length === 0) return;
|
||||
|
||||
this.statusLoading = true;
|
||||
return this.mastodonService.getAccountStatuses(selectedAccounts[0], account.id, false, false, true, null, null, 40)
|
||||
.then((result: Status[]) => {
|
||||
for (const status of result) {
|
||||
const wrapper = new StatusWrapper(status, selectedAccounts[0]);
|
||||
this.statuses.push(wrapper);
|
||||
}
|
||||
this.statusLoading = false;
|
||||
});
|
||||
// .catch(err => {
|
||||
|
||||
// })
|
||||
// .then(() => {
|
||||
// this.statusLoading = false;
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
8
src/app/pipes/time-ago.pipe.spec.ts
Normal file
8
src/app/pipes/time-ago.pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { TimeAgoPipe } from './time-ago.pipe';
|
||||
|
||||
describe('TimeAgoPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new TimeAgoPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
104
src/app/pipes/time-ago.pipe.ts
Normal file
104
src/app/pipes/time-ago.pipe.ts
Normal file
@ -0,0 +1,104 @@
|
||||
//https://github.com/AndrewPoyntz/time-ago-pipe/issues/6#issuecomment-313726956
|
||||
|
||||
import { Pipe, PipeTransform, NgZone } from "@angular/core";
|
||||
import { Observable, Observer } from 'rxjs';
|
||||
|
||||
interface processOutput {
|
||||
text: string; // Convert timestamp to string
|
||||
timeToUpdate: number; // Time until update in milliseconds
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'timeAgo',
|
||||
pure: true
|
||||
})
|
||||
export class TimeAgoPipe implements PipeTransform {
|
||||
|
||||
constructor(private ngZone: NgZone) { }
|
||||
|
||||
private process = (timestamp: number): processOutput => {
|
||||
let text: string;
|
||||
let timeToUpdate: number;
|
||||
|
||||
const now = new Date();
|
||||
|
||||
// Time ago in milliseconds
|
||||
const timeAgo: number = now.getTime() - timestamp;
|
||||
|
||||
const seconds = timeAgo / 1000;
|
||||
const minutes = seconds / 60;
|
||||
const hours = minutes / 60;
|
||||
const days = hours / 24;
|
||||
// const months = days / 30.416;
|
||||
// const years = days / 365;
|
||||
|
||||
if (seconds <= 60) {
|
||||
text = Math.round(seconds) + 's';
|
||||
} else if (minutes <= 90) {
|
||||
text = Math.round(minutes) + 'm';
|
||||
} else if (hours <= 24) {
|
||||
text = Math.round(hours) + 'h';
|
||||
} else {
|
||||
text = Math.round(days) + 'd';
|
||||
}
|
||||
|
||||
if (minutes < 1) {
|
||||
// update every 2 secs
|
||||
timeToUpdate = 2 * 1000;
|
||||
} else if (hours < 1) {
|
||||
// update every 30 secs
|
||||
timeToUpdate = 30 * 1000;
|
||||
} else if (days < 1) {
|
||||
// update every 5 mins
|
||||
timeToUpdate = 300 * 1000;
|
||||
} else {
|
||||
// update every hour
|
||||
timeToUpdate = 3600 * 1000;
|
||||
}
|
||||
|
||||
return {
|
||||
text,
|
||||
timeToUpdate
|
||||
};
|
||||
}
|
||||
|
||||
public transform = (value: string | Date): Observable<string> => {
|
||||
let d: Date;
|
||||
if (value instanceof Date) {
|
||||
d = value;
|
||||
}
|
||||
else {
|
||||
d = new Date(value);
|
||||
}
|
||||
// time value in milliseconds
|
||||
const timestamp = d.getTime();
|
||||
|
||||
let timeoutID: any;
|
||||
|
||||
return Observable.create((observer: Observer<string>) => {
|
||||
let latestText = '';
|
||||
|
||||
// Repeatedly set new timeouts for new update checks.
|
||||
const registerUpdate = () => {
|
||||
const processOutput = this.process(timestamp);
|
||||
if (processOutput.text !== latestText) {
|
||||
latestText = processOutput.text;
|
||||
this.ngZone.run(() => {
|
||||
observer.next(latestText);
|
||||
});
|
||||
}
|
||||
timeoutID = setTimeout(registerUpdate, processOutput.timeToUpdate);
|
||||
};
|
||||
|
||||
this.ngZone.runOutsideAngular(registerUpdate);
|
||||
|
||||
// Return teardown function
|
||||
const teardownFunction = () => {
|
||||
if (timeoutID) {
|
||||
clearTimeout(timeoutID);
|
||||
}
|
||||
};
|
||||
return teardownFunction;
|
||||
});
|
||||
}
|
||||
}
|
@ -117,6 +117,23 @@ export class MastodonService {
|
||||
return this.httpClient.get<Results>(route, { headers: headers }).toPromise()
|
||||
}
|
||||
|
||||
getAccountStatuses(account: AccountInfo, targetAccountId: number, onlyMedia: boolean, onlyPinned: boolean, excludeReplies: boolean, maxId: string, sinceId: string, limit: number = 20): Promise<Status[]>{
|
||||
const route = `https://${account.instance}${this.apiRoutes.getAccountStatuses}`.replace('{0}', targetAccountId.toString());
|
||||
let params = `?only_media=${onlyMedia}&pinned=${onlyPinned}&exclude_replies=${excludeReplies}&limit=${limit}`;
|
||||
|
||||
if(maxId) params += `&max_id=${maxId}`;
|
||||
if(sinceId) params += `&since_id=${sinceId}`;
|
||||
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Status[]>(route+params, { headers: headers }).toPromise();
|
||||
}
|
||||
|
||||
searchAccount(account: AccountInfo, query: string, limit: number = 40, following: boolean = false): Promise<Account[]>{
|
||||
const route = `https://${account.instance}${this.apiRoutes.searchForAccounts}?q=${query}&limit=${limit}&following=${following}`;
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
return this.httpClient.get<Account[]>(route, { headers: headers }).toPromise()
|
||||
}
|
||||
|
||||
reblog(account: AccountInfo, status: Status): Promise<Status> {
|
||||
const route = `https://${account.instance}${this.apiRoutes.reblogStatus}`.replace('{0}', status.id);
|
||||
const headers = new HttpHeaders({ 'Authorization': `Bearer ${account.token.access_token}` });
|
||||
|
@ -39,11 +39,12 @@ export class StreamingWrapper {
|
||||
this.eventSource.onmessage = x => this.statusParsing(<WebSocketEvent>JSON.parse(x.data));
|
||||
this.eventSource.onerror = x => this.webSocketGotError(x);
|
||||
this.eventSource.onopen = x => console.log(x);
|
||||
this.eventSource.onclose = x => this.webSocketClosed(route, x);
|
||||
this.eventSource.onclose = x => this.webSocketClosed(route, x);
|
||||
}
|
||||
|
||||
private errorClosing: boolean;
|
||||
private webSocketGotError(x: Event) {
|
||||
console.log(x);
|
||||
this.errorClosing = true;
|
||||
}
|
||||
|
||||
@ -52,7 +53,36 @@ export class StreamingWrapper {
|
||||
console.log(x);
|
||||
|
||||
if (this.errorClosing) {
|
||||
this.mastodonService.getTimeline(this.accountInfo, this.streamType, null, this.since_id)
|
||||
|
||||
this.pullNewStatuses(domain);
|
||||
|
||||
// this.mastodonService.getTimeline(this.accountInfo, this.streamType, null, this.since_id)
|
||||
// .then((status: Status[]) => {
|
||||
// // status = status.sort((n1, n2) => { return (<number>n1.id) < (<number>n2.id); });
|
||||
// status = status.sort((a, b) => a.id.localeCompare(b.id));
|
||||
// for (const s of status) {
|
||||
// const update = new StatusUpdate();
|
||||
// update.status = s;
|
||||
// update.type = EventEnum.update;
|
||||
// this.since_id = update.status.id;
|
||||
// this.statusUpdateSubjet.next(update);
|
||||
// }
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.error(err);
|
||||
// })
|
||||
// .then(() => {
|
||||
// setTimeout(() => { this.start(domain) }, 20 * 1000);
|
||||
// });
|
||||
|
||||
this.errorClosing = false;
|
||||
} else {
|
||||
setTimeout(() => { this.start(domain) }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private pullNewStatuses(domain){
|
||||
this.mastodonService.getTimeline(this.accountInfo, this.streamType, null, this.since_id)
|
||||
.then((status: Status[]) => {
|
||||
// status = status.sort((n1, n2) => { return (<number>n1.id) < (<number>n2.id); });
|
||||
status = status.sort((a, b) => a.id.localeCompare(b.id));
|
||||
@ -68,13 +98,9 @@ export class StreamingWrapper {
|
||||
console.error(err);
|
||||
})
|
||||
.then(() => {
|
||||
setTimeout(() => { this.start(domain) }, 20 * 1000);
|
||||
// setTimeout(() => { this.start(domain) }, 20 * 1000);
|
||||
setTimeout(() => { this.pullNewStatuses(domain) }, 15 * 1000);
|
||||
});
|
||||
|
||||
this.errorClosing = false;
|
||||
} else {
|
||||
setTimeout(() => { this.start(domain) }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private statusParsing(event: WebSocketEvent) {
|
||||
|
15
src/app/services/tools.service.spec.ts
Normal file
15
src/app/services/tools.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { ToolsService } from './tools.service';
|
||||
|
||||
xdescribe('ToolsService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ToolsService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([ToolsService], (service: ToolsService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
20
src/app/services/tools.service.ts
Normal file
20
src/app/services/tools.service.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Store } from '@ngxs/store';
|
||||
|
||||
import { AccountInfo } from '../states/accounts.state';
|
||||
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ToolsService {
|
||||
|
||||
constructor( private readonly store: Store) { }
|
||||
|
||||
|
||||
getSelectedAccounts(): AccountInfo[] {
|
||||
var regAccounts = <AccountInfo[]>this.store.snapshot().registeredaccounts.accounts;
|
||||
return regAccounts.filter(x => x.isSelected);
|
||||
}
|
||||
|
||||
}
|
@ -2,4 +2,24 @@
|
||||
width: 40px;
|
||||
display: block;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.flexcroll {
|
||||
scrollbar-face-color: #08090d;
|
||||
scrollbar-shadow-color: #08090d;
|
||||
scrollbar-highlight-color: #08090d;
|
||||
scrollbar-3dlight-color: #08090d;
|
||||
scrollbar-darkshadow-color: #08090d;
|
||||
scrollbar-track-color: #08090d;
|
||||
scrollbar-arrow-color: #08090d;
|
||||
&::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
-webkit-border-radius: 0px;
|
||||
border-radius: 0px;
|
||||
// background: #08090d;
|
||||
background: lighten($color-primary, 5);
|
||||
// -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user