diff --git a/AGENTS.md b/AGENTS.md index 07074df6..064baae1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,8 @@ Sengi is a multi-account Mastodon/Pleroma desktop client built with Angular 7, T When editing colors, use css variables following the current themes implementation. The themes colors are stored in the *.ts files in the `/src/app/themes/implementations` folder, they MUST be updated accordingly. -And don't forget to update the `/src/sass/_variables.scss` file when doing so. + +You MUST update the `/src/sass/_variables.scss` file accordingly, when updating theme's variables. ## Commands diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 427f9f7d..635990f6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -92,6 +92,7 @@ import { LabelsTutorialComponent } from './components/tutorial-enhanced/labels-t import { ThankyouTutorialComponent } from './components/tutorial-enhanced/thankyou-tutorial/thankyou-tutorial.component'; import { StatusTranslateComponent } from './components/stream/status/status-translate/status-translate.component'; import { ThemeModule } from "./themes/theme.module"; +import { QuoteComponent } from './components/stream/status/quote/quote.component'; const routes: Routes = [ { path: "", component: StreamsMainDisplayComponent }, @@ -162,7 +163,8 @@ const routes: Routes = [ NotificationsTutorialComponent, LabelsTutorialComponent, ThankyouTutorialComponent, - StatusTranslateComponent + StatusTranslateComponent, + QuoteComponent ], entryComponents: [ EmojiPickerComponent diff --git a/src/app/components/floating-column/settings/settings.component.ts b/src/app/components/floating-column/settings/settings.component.ts index 4b695a90..516feee5 100644 --- a/src/app/components/floating-column/settings/settings.component.ts +++ b/src/app/components/floating-column/settings/settings.component.ts @@ -290,11 +290,7 @@ export class SettingsComponent implements OnInit, OnDestroy { onThemeChange(themeId: number) { const castedType = themeId; - console.warn(themeId); - console.warn(this.themeList); - const newTheme = this.themeList.find(x => x.theme_type == castedType); - console.warn(newTheme); if(newTheme) this.themeService.setTheme(newTheme); } diff --git a/src/app/components/stream/status/databinded-text/databinded-text.component.scss b/src/app/components/stream/status/databinded-text/databinded-text.component.scss index a0ecc9b0..5eeb9448 100644 --- a/src/app/components/stream/status/databinded-text/databinded-text.component.scss +++ b/src/app/components/stream/status/databinded-text/databinded-text.component.scss @@ -58,7 +58,7 @@ $expand-color: $column-color; color: $status-links-color; } - & .invisible { + & .invisible, & .quote-inline { display: none; } diff --git a/src/app/components/stream/status/quote/quote.component.html b/src/app/components/stream/status/quote/quote.component.html new file mode 100644 index 00000000..b75b0a55 --- /dev/null +++ b/src/app/components/stream/status/quote/quote.component.html @@ -0,0 +1,32 @@ +
+ + + + + + + + {{displayStatus.account.acct}} + + + + + + + + +
+ Quoted another post +
+
+
+ {{ getReadableStatus(quoteState) }} +
+
+ {{ error }} +
\ No newline at end of file diff --git a/src/app/components/stream/status/quote/quote.component.scss b/src/app/components/stream/status/quote/quote.component.scss new file mode 100644 index 00000000..f6392139 --- /dev/null +++ b/src/app/components/stream/status/quote/quote.component.scss @@ -0,0 +1,97 @@ +@import "variables"; +@import "commons"; + +.quote { + border: 1px solid $quote-border-color; + border-radius: 3px; + padding: 3px; + margin: 15px 2px 0 $avatar-column-space; + position: relative; + + &__unaccepted { + color: $quote-unaccepted-color; + text-align: center; + font-size: 12px; + padding: 5px 10px; + } + + &__open-quote-btn { + position: absolute; + top: 0px; + right: 1px; + color: $status-secondary-color; + text-decoration: none; + padding: 7px 7px; + font-size: 7px; + z-index: 10; + + &:hover { + color: var(--status-secondary-color-lighten-20); + } + } + + &__profile-link { + display: flex; + align-items: center; + text-decoration: none; + margin-bottom: 5px; + padding-right: 18px; + + color: $status-secondary-color; + overflow: hidden; + + &:hover { + color: var(--status-secondary-color-lighten-20); + } + } + + &__avatar { + width: 30px; + height: 30px; + border-radius: 2px; + margin-right: 4px; + flex-shrink: 0; + } + + &__name { + display: flex; + flex-direction: column; + align-items: flex-start; + overflow: hidden; + white-space: nowrap; + + &--displayname { + line-height: 1; + color: $status-primary-color; + text-overflow: ellipsis; + overflow: hidden; + max-width: 100%; + } + + &--username { + line-height: 1; + text-overflow: ellipsis; + overflow: hidden; + max-width: 100%; + } + } + + &__attachments { + display: block; + margin-top: 5px; + } + + &__sub-quote { + margin: 5px 0 0 0; + padding: 2px 0 2px 5px; + color: $quote-sub-quote-color; + background-color: $quote-sub-quote-background; + border-radius: 3px; + } + + &__poll { + display: block; + margin-top: 5px; + } +} + diff --git a/src/app/components/stream/status/quote/quote.component.spec.ts b/src/app/components/stream/status/quote/quote.component.spec.ts new file mode 100644 index 00000000..38416007 --- /dev/null +++ b/src/app/components/stream/status/quote/quote.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { QuoteComponent } from './quote.component'; + +xdescribe('QuoteComponent', () => { + let component: QuoteComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ QuoteComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(QuoteComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/stream/status/quote/quote.component.ts b/src/app/components/stream/status/quote/quote.component.ts new file mode 100644 index 00000000..4a96912d --- /dev/null +++ b/src/app/components/stream/status/quote/quote.component.ts @@ -0,0 +1,158 @@ +import { Component, OnInit, Input, Output, EventEmitter, ApplicationRef } from '@angular/core'; +import { faCircle } from '@fortawesome/free-solid-svg-icons'; + +import { Status, Account, Quote, ShallowQuote } from '../../../../services/models/mastodon.interfaces'; +import { EmojiConverter, EmojiTypeEnum } from '../../../../tools/emoji.tools'; +import { OpenThreadEvent, ToolsService } from '../../../../services/tools.service'; +import { SettingsService } from '../../../../services/settings.service'; +import { AccountInfo } from '../../../../states/accounts.state'; +import { MastodonWrapperService } from '../../../../services/mastodon-wrapper.service'; +import { StatusWrapper } from '../../../../models/common.model'; + +@Component({ + selector: 'app-quote', + templateUrl: './quote.component.html', + styleUrls: ['./quote.component.scss'] +}) +export class QuoteComponent implements OnInit { + private emojiConverter = new EmojiConverter(); + faCircle = faCircle; + + displayStatus: Status; + displayStatusWrapper: StatusWrapper; + quoteState: 'pending' | 'accepted' | 'rejected' | 'revoked' | 'deleted' | 'unauthorized' | 'blocked_account' | 'blocked_domain' | 'muted_account'; + error: string; + + private quote: Quote; + private shallowQuote: ShallowQuote; + + @Output() browseAccountEvent = new EventEmitter(); + @Output() browseHashtagEvent = new EventEmitter(); + @Output() browseThreadEvent = new EventEmitter(); + + @Input() accountInfo: AccountInfo; + + @Input('quote') + set setQuote(value: Quote | ShallowQuote) { + + this.quoteState = value.state; + //this.quoteState = "revoked"; + + let quoteValue = value; + + if (quoteValue.quoted_status) { // Quote + + this.quote = quoteValue; + this.displayStatus = quoteValue.quoted_status; + + if (!this.displayStatus.account.display_name) { + this.displayStatus.account.display_name = this.displayStatus.account.username; + } + + const statusContent = this.emojiConverter.applyEmojis(this.displayStatus.emojis, this.displayStatus.content, EmojiTypeEnum.medium); + this.statusContent = this.ensureMentionAreDisplayed(statusContent); + + } else { // ShallowQuote + this.shallowQuote = value; + this.mastodonService.getStatus(this.accountInfo, this.shallowQuote.quoted_status_id) + .then(status => { + this.displayStatus = status; + this.appRef.tick(); + }) + .catch(err => { + console.error(err); + this.error = "Error retrieving status."; + this.appRef.tick(); + }); + } + + this.displayStatusWrapper = new StatusWrapper(this.displayStatus, this.accountInfo, false, false); + } + + statusContent: string; + private freezeAvatarEnabled: boolean; + + constructor( + private appRef: ApplicationRef, + private readonly settingsService: SettingsService, + private readonly toolsService: ToolsService, + private readonly mastodonService: MastodonWrapperService) { + this.freezeAvatarEnabled = this.settingsService.getSettings().enableFreezeAvatar; + } + + ngOnInit() { + } + + // TODO: refactorise this + private ensureMentionAreDisplayed(data: string): string { + const mentions = this.displayStatus.mentions; + if (!mentions || mentions.length === 0) { + return data; + } + + let textMentions = ''; + for (const m of mentions) { + if (!data.includes(m.url)) { + textMentions += ` + @ + ${m.username} + + `; + } + } + if (textMentions !== '') { + data = textMentions + data; + } + return data; + } + + getReadableStatus(state: 'pending' | 'accepted' | 'rejected' | 'revoked' | 'deleted' | 'unauthorized' | 'blocked_account' | 'blocked_domain' | 'muted_account'): string { + switch (state) { + case "pending": return "Waiting for author authorization." + case "rejected": return "Author rejected quote, can't be displayed." + case "revoked": return "Author revoked quote, can't be displayed."; + case "deleted": return "Post deleted."; + case "unauthorized": return "Not authorized."; + case "blocked_account": return "Blocked account."; + case "blocked_domain": return "Blocked domain."; + case "muted_account": return "Muted account."; + } + return ""; + } + + accountSelected(accountName: string): void { + this.browseAccountEvent.next(accountName); + } + + hashtagSelected(hashtag: string): void { + this.browseHashtagEvent.next(hashtag); + } + + textSelected(): boolean { + let status = this.displayStatus; + const accountInfo = this.accountInfo; + + if (status.reblog) { + status = status.reblog; + } + + const openThread = new OpenThreadEvent(status, accountInfo); + this.browseThreadEvent.next(openThread); + return false; + } + + getAvatar(acc: Account): string { + if (this.freezeAvatarEnabled) { + return acc.avatar_static; + } else { + return acc.avatar; + } + } + + openAccount(account: Account): boolean { + let accountName = this.toolsService.getAccountFullHandle(account); + this.browseAccountEvent.emit(accountName); + return false; + } + +} diff --git a/src/app/components/stream/status/status.component.html b/src/app/components/stream/status/status.component.html index 0d615716..91edc6cc 100644 --- a/src/app/components/stream/status/status.component.html +++ b/src/app/components/stream/status/status.component.html @@ -130,10 +130,17 @@ [attachments]="displayedStatus.media_attachments"> + + - + diff --git a/src/app/services/instances-info.service.ts b/src/app/services/instances-info.service.ts index 7f22b5c0..40ce1f53 100644 --- a/src/app/services/instances-info.service.ts +++ b/src/app/services/instances-info.service.ts @@ -31,7 +31,7 @@ export class InstancesInfoService { const instanceV1 = instance; if (instanceV1 && instanceV1.max_toot_chars) return instanceV1.max_toot_chars; - if(instanceV1 && instanceV1.configuration && instanceV1.configuration.statuses && instanceV1.configuration.statuses.max_characters) + if (instanceV1 && instanceV1.configuration && instanceV1.configuration.statuses && instanceV1.configuration.statuses.max_characters) return instanceV1.configuration.statuses.max_characters; } @@ -74,16 +74,18 @@ export class InstancesInfoService { if (!this.cachedTranslationAvailability[instance]) { this.cachedTranslationAvailability[instance] = this.mastodonService.getInstance(instance) .then((instance: Instance) => { - if (+instance.version.split('.')[0] >= 4) { - const instanceV2 = instance; - if (instanceV2 - && instanceV2.configuration - && instanceV2.configuration.translation) - return instanceV2.configuration.translation.enabled; - } else { - const instanceV1 = instance; - if (instanceV1 && instanceV1.max_toot_chars) - return false; + if (instance) { + if (+instance.version.split('.')[0] >= 4) { + const instanceV2 = instance; + if (instanceV2 + && instanceV2.configuration + && instanceV2.configuration.translation) + return instanceV2.configuration.translation.enabled; + } else { + const instanceV1 = instance; + if (instanceV1 && instanceV1.max_toot_chars) + return false; + } } return false; diff --git a/src/app/services/media.service.ts b/src/app/services/media.service.ts index b3c72470..36ac6240 100644 --- a/src/app/services/media.service.ts +++ b/src/app/services/media.service.ts @@ -85,7 +85,7 @@ export class MediaService { this.mediaSubject.next(medias); }) .catch((err) => { - console.warn('failing update'); + console.error('failing update'); this.notificationService.notifyHttpError(err, account); }); } diff --git a/src/app/services/models/mastodon.interfaces.ts b/src/app/services/models/mastodon.interfaces.ts index 860bf6a4..f197dc67 100644 --- a/src/app/services/models/mastodon.interfaces.ts +++ b/src/app/services/models/mastodon.interfaces.ts @@ -221,6 +221,22 @@ export interface FilterResult { status_matches: string[]; } +export interface Quote { + state: 'pending' | 'accepted' | 'rejected' | 'revoked' | 'deleted' | 'unauthorized' | 'blocked_account' | 'blocked_domain' | 'muted_account'; + quoted_status: Status; +} + +export interface ShallowQuote { + state: 'pending' | 'accepted' | 'rejected' | 'revoked' | 'deleted' | 'unauthorized' | 'blocked_account' | 'blocked_domain' | 'muted_account'; + quoted_status_id: string; +} + +export interface QuoteApproval { + automatic: 'public' | 'followers' | 'following' | 'unsupported_policy' []; + manual: 'public' | 'followers' | 'following' | 'unsupported_policy' []; + current_user: 'manual' | 'denied' | 'unknown'; +} + export interface Status { id: string; uri: string; @@ -234,7 +250,10 @@ export interface Status { edited_at: string; reblogs_count: number; replies_count: number; - favourites_count: string; + favourites_count: number; + quotes_count: number; + quote: Quote | ShallowQuote; + quote_approval: QuoteApproval; reblogged: boolean; favourited: boolean; sensitive: boolean; diff --git a/src/app/themes/implementations/default.ts b/src/app/themes/implementations/default.ts index 8a85924b..3ec03828 100644 --- a/src/app/themes/implementations/default.ts +++ b/src/app/themes/implementations/default.ts @@ -325,6 +325,11 @@ export const defaultTheme: Theme = { "--attachment-icon-overlay": "#ffffff", "--attachment-icon-shadow": "#4e4e4e", - "--attachment-icon-shadow-hover": "#4e4e4e" + "--attachment-icon-shadow-hover": "#4e4e4e", + + "--quote-border-color": "#222736", + "--quote-unaccepted-color": "#e46161", + "--quote-sub-quote-color": "#4e5572", + "--quote-sub-quote-background": "#0c0c10" } }; \ No newline at end of file diff --git a/src/app/themes/implementations/light.ts b/src/app/themes/implementations/light.ts index 6b4514a3..f195c1d0 100644 --- a/src/app/themes/implementations/light.ts +++ b/src/app/themes/implementations/light.ts @@ -333,6 +333,11 @@ export const lightTheme: Theme = { "--attachment-icon-overlay": "#2d3134", "--attachment-icon-shadow": "#9ca3af", - "--attachment-icon-shadow-hover": "#6b7280" + "--attachment-icon-shadow-hover": "#6b7280", + + "--quote-border-color": "#9ca7b3", + "--quote-unaccepted-color": "#e00b0b", + "--quote-sub-quote-color": "#ffffff", + "--quote-sub-quote-background": "#bdc1ca" } }; \ No newline at end of file diff --git a/src/sass/_variables.scss b/src/sass/_variables.scss index 132f9134..2f1b1940 100644 --- a/src/sass/_variables.scss +++ b/src/sass/_variables.scss @@ -294,6 +294,10 @@ --attachment-icon-shadow: #4e4e4e; --attachment-icon-shadow-hover: #4e4e4e; + --quote-border-color: orange; + --quote-unaccepted-color: orangered; + --quote-sub-quote-color: purple; + --quote-sub-quote-background: aqua; --color-primary-lighter-5: hsl(from var(var(--color-primary)) h s calc(l + 5%)); --color-primary-lighter-15: hsl(from var(var(--color-primary)) h s calc(l + 15%)); @@ -515,4 +519,9 @@ $media-icon: var(--media-button-color); $attachment-icon-overlay: var(--attachment-icon-overlay); $attachment-icon-shadow: var(--attachment-icon-shadow); -$attachment-icon-shadow-hover: var(--attachment-icon-shadow-hover); \ No newline at end of file +$attachment-icon-shadow-hover: var(--attachment-icon-shadow-hover); + +$quote-border-color: var(--quote-border-color); +$quote-unaccepted-color: var(--quote-unaccepted-color); +$quote-sub-quote-color: var(--quote-sub-quote-color); +$quote-sub-quote-background: var(--quote-sub-quote-background); \ No newline at end of file