├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── build.yml │ └── docker.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── Dockerfile ├── LICENSE ├── README.md ├── Static.Dockerfile ├── angular.json ├── eslint.config.js ├── express.tokens.ts ├── fly.toml ├── images ├── 01.png └── 02.png ├── nginx.conf ├── ngsw-config.json ├── package-lock.json ├── package.json ├── server.ts ├── src ├── app │ ├── animations │ │ ├── fade-in.animation.ts │ │ └── show-or-hide.animation.ts │ ├── app-initialization.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.server.ts │ ├── app.module.ts │ ├── common │ │ ├── always-error-state-mather.ts │ │ ├── custom-reuse-strategy.ts │ │ ├── dirty-error-state-matcher.ts │ │ ├── responsive.ts │ │ └── reusable-gallery-page.ts │ ├── components │ │ ├── angular-material.module.ts │ │ ├── components.module.ts │ │ ├── core │ │ │ ├── footer │ │ │ │ ├── footer.component.html │ │ │ │ ├── footer.component.scss │ │ │ │ └── footer.component.ts │ │ │ └── header │ │ │ │ ├── header.component.html │ │ │ │ ├── header.component.scss │ │ │ │ └── header.component.ts │ │ └── widgets │ │ │ ├── article-inline │ │ │ ├── article-inline.component.html │ │ │ ├── article-inline.component.scss │ │ │ └── article-inline.component.ts │ │ │ ├── avatar │ │ │ ├── avatar-size.ts │ │ │ ├── avatar.component.html │ │ │ ├── avatar.component.scss │ │ │ └── avatar.component.ts │ │ │ ├── blurhash-image │ │ │ ├── blurhash-image.component.html │ │ │ ├── blurhash-image.component.scss │ │ │ └── blurhash-image.component.ts │ │ │ ├── business-card │ │ │ ├── business-card.component.html │ │ │ ├── business-card.component.scss │ │ │ └── business-card.component.ts │ │ │ ├── category-gallery-item │ │ │ ├── category-gallery-item.component.html │ │ │ ├── category-gallery-item.component.scss │ │ │ └── category-gallery-item.component.ts │ │ │ ├── category-gallery │ │ │ ├── category-gallery.component.html │ │ │ ├── category-gallery.component.scss │ │ │ └── category-gallery.component.ts │ │ │ ├── category-list │ │ │ ├── category-list.component.html │ │ │ ├── category-list.component.scss │ │ │ └── category-list.component.ts │ │ │ ├── comment-reply │ │ │ ├── comment-reply.component.html │ │ │ ├── comment-reply.component.scss │ │ │ └── comment-reply.component.ts │ │ │ ├── domain-blocks │ │ │ ├── domain-blocks.component.html │ │ │ ├── domain-blocks.component.scss │ │ │ └── domain-blocks.component.ts │ │ │ ├── follow-buttons-section │ │ │ ├── follow-buttons-section.component.html │ │ │ ├── follow-buttons-section.component.scss │ │ │ └── follow-buttons-section.component.ts │ │ │ ├── gallery │ │ │ ├── gallery.component.html │ │ │ ├── gallery.component.scss │ │ │ └── gallery.component.ts │ │ │ ├── general-settings │ │ │ ├── general-settings.component.html │ │ │ ├── general-settings.component.scss │ │ │ └── general-settings.component.ts │ │ │ ├── hashtag-gallery-item │ │ │ ├── hashtag-gallery-item.component.html │ │ │ ├── hashtag-gallery-item.component.scss │ │ │ └── hashtag-gallery-item.component.ts │ │ │ ├── hashtag-gallery │ │ │ ├── hashtag-gallery.component.html │ │ │ ├── hashtag-gallery.component.scss │ │ │ └── hashtag-gallery.component.ts │ │ │ ├── hashtags-search │ │ │ ├── hashtags-search.component.html │ │ │ ├── hashtags-search.component.scss │ │ │ └── hashtags-search.component.ts │ │ │ ├── home-signin │ │ │ ├── home-signin.component.html │ │ │ ├── home-signin.component.scss │ │ │ └── home-signin.component.ts │ │ │ ├── home-signout │ │ │ ├── home-signout.component.html │ │ │ ├── home-signout.component.scss │ │ │ └── home-signout.component.ts │ │ │ ├── image │ │ │ ├── image.component.html │ │ │ ├── image.component.scss │ │ │ └── image.component.ts │ │ │ ├── instance-rules │ │ │ ├── instance-rules.component.html │ │ │ ├── instance-rules.component.scss │ │ │ └── instance-rules.component.ts │ │ │ ├── mini-user-card │ │ │ ├── mini-user-card.component.html │ │ │ ├── mini-user-card.component.scss │ │ │ └── mini-user-card.component.ts │ │ │ ├── password │ │ │ ├── password.component.html │ │ │ ├── password.component.scss │ │ │ └── password.component.ts │ │ │ ├── status-properties │ │ │ ├── status-properties.component.html │ │ │ ├── status-properties.component.scss │ │ │ └── status-properties.component.ts │ │ │ ├── statuses-search │ │ │ ├── statuses-search.component.html │ │ │ ├── statuses-search.component.scss │ │ │ └── statuses-search.component.ts │ │ │ ├── upload-photo │ │ │ ├── upload-photo.component.html │ │ │ ├── upload-photo.component.scss │ │ │ └── upload-photo.component.ts │ │ │ ├── user-card │ │ │ ├── user-card.component.html │ │ │ ├── user-card.component.scss │ │ │ └── user-card.component.ts │ │ │ ├── user-popover │ │ │ ├── user-popover.component.html │ │ │ ├── user-popover.component.scss │ │ │ └── user-popover.component.ts │ │ │ ├── user-selector │ │ │ ├── user-selector.component.html │ │ │ ├── user-selector.component.scss │ │ │ └── user-selector.component.ts │ │ │ ├── users-card │ │ │ ├── users-card.component.html │ │ │ ├── users-card.component.scss │ │ │ └── users-card.component.ts │ │ │ ├── users-gallery-item │ │ │ ├── users-gallery-item.component.html │ │ │ ├── users-gallery-item.component.scss │ │ │ └── users-gallery-item.component.ts │ │ │ └── users-gallery │ │ │ ├── users-gallery.component.html │ │ │ ├── users-gallery.component.scss │ │ │ └── users-gallery.component.ts │ ├── dialogs │ │ ├── category-dialog │ │ │ ├── category-hashtag-item.component.html │ │ │ ├── category-hashtag-item.component.scss │ │ │ ├── category-hashtag-item.component.ts │ │ │ ├── category.dialog.html │ │ │ ├── category.dialog.scss │ │ │ └── category.dialog.ts │ │ ├── change-email-dialog │ │ │ ├── change-email.dialog.html │ │ │ └── change-email.dialog.ts │ │ ├── change-password-dialog │ │ │ ├── change-password.dialog.html │ │ │ └── change-password.dialog.ts │ │ ├── confirmation-dialog │ │ │ ├── confirmation.dialog.html │ │ │ ├── confirmation.dialog.scss │ │ │ └── confirmation.dialog.ts │ │ ├── content-warning-dialog │ │ │ ├── content-warning.dialog.html │ │ │ └── content-warning.dialog.ts │ │ ├── create-alias-dialog │ │ │ ├── create-alias.dialog.html │ │ │ ├── create-alias.dialog.scss │ │ │ └── create-alias.dialog.ts │ │ ├── delete-account-dialog │ │ │ ├── delete-account.dialog.html │ │ │ └── delete-account.dialog.ts │ │ ├── delete-status-dialog │ │ │ ├── delete-status.dialog.html │ │ │ └── delete-status.dialog.ts │ │ ├── dialogs.module.ts │ │ ├── disable-two-factor-token │ │ │ ├── disable-two-factor-token.dialog.html │ │ │ └── disable-two-factor-token.dialog.ts │ │ ├── enable-two-factor-token │ │ │ ├── enable-two-factor-token.dialog.html │ │ │ ├── enable-two-factor-token.dialog.scss │ │ │ └── enable-two-factor-token.dialog.ts │ │ ├── error-item-dialog │ │ │ ├── error-item.dialog.html │ │ │ └── error-item.dialog.ts │ │ ├── following-import-accounts-dialog │ │ │ ├── following-import-accounts.dialog.html │ │ │ └── following-import-accounts.dialog.ts │ │ ├── instance-blocked-domain-dialog │ │ │ ├── instance-blocked-domain.dialog.html │ │ │ ├── instance-blocked-domain.dialog.scss │ │ │ └── instance-blocked-domain.dialog.ts │ │ ├── instance-rule-dialog │ │ │ ├── instance-rule.dialog.html │ │ │ ├── instance-rule.dialog.scss │ │ │ └── instance-rule.dialog.ts │ │ ├── mute-account-dialog │ │ │ ├── mute-account.dialog.html │ │ │ └── mute-account.dialog.ts │ │ ├── notification-settings-dialog │ │ │ ├── notification-settings.dialog.html │ │ │ ├── notification-settings.dialog.scss │ │ │ └── notification-settings.dialog.ts │ │ ├── profile-code-dialog │ │ │ ├── profile-code.dialog.html │ │ │ ├── profile-code.dialog.scss │ │ │ └── profile-code.dialog.ts │ │ ├── report-details-dialog │ │ │ ├── report-details.dialog.html │ │ │ └── report-details.dialog.ts │ │ ├── report-dialog │ │ │ ├── report-data.ts │ │ │ ├── report.dialog.html │ │ │ └── report.dialog.ts │ │ ├── share-business-card-dialog │ │ │ ├── share-business-card.dialog.html │ │ │ └── share-business-card.dialog.ts │ │ ├── status-text-template-dialog │ │ │ ├── status-text-template.dialog.html │ │ │ └── status-text-template.dialog.ts │ │ ├── update-shared-business-card │ │ │ ├── update-shared-business-card.dialog.html │ │ │ └── update-shared-business-card.dialog.ts │ │ ├── user-roles-dialog │ │ │ ├── user-roles.dialog.html │ │ │ └── user-roles.dialog.ts │ │ └── users-dialog │ │ │ ├── users-dialog-context.ts │ │ │ ├── users.dialog.html │ │ │ ├── users.dialog.scss │ │ │ └── users.dialog.ts │ ├── directives │ │ ├── directive.module.ts │ │ ├── href-to-router-link.directive.ts │ │ ├── infinite-scroll.directive.ts │ │ ├── input-activity.directive.ts │ │ ├── lazy-load.directive.ts │ │ └── note-processor.directive.ts │ ├── errors │ │ ├── custom-error.ts │ │ ├── forbidden-error.ts │ │ ├── object-not-found-error.ts │ │ ├── page-not-found-error.ts │ │ └── server-refresh-token-not-exists-error.ts │ ├── handlers │ │ └── global-error-handler.ts │ ├── interceptors │ │ └── api.interceptor.ts │ ├── models │ │ ├── account-mode.ts │ │ ├── archive-status.ts │ │ ├── archive.ts │ │ ├── article-file-info.ts │ │ ├── article-visibility.ts │ │ ├── article.ts │ │ ├── attachment-description.ts │ │ ├── attachment-hashtag.ts │ │ ├── attachment.ts │ │ ├── auth-client.ts │ │ ├── boolean-result.ts │ │ ├── business-card-avatar.ts │ │ ├── business-card-field.ts │ │ ├── business-card.ts │ │ ├── category-hashtag.ts │ │ ├── category-statuses.ts │ │ ├── category.ts │ │ ├── change-email.ts │ │ ├── change-password.ts │ │ ├── configuration-attachments.ts │ │ ├── configuration-statuses.ts │ │ ├── configuration.ts │ │ ├── confirm-email-mode.ts │ │ ├── confirm-email.ts │ │ ├── content-warning.ts │ │ ├── context-timeline.ts │ │ ├── country.ts │ │ ├── email-secure-method.ts │ │ ├── error-item-source.ts │ │ ├── error-item.ts │ │ ├── event-type.ts │ │ ├── exif.ts │ │ ├── file-info.ts │ │ ├── flexi-field.ts │ │ ├── following-import-item-status.ts │ │ ├── following-import-item.ts │ │ ├── following-import-status.ts │ │ ├── following-import.ts │ │ ├── forgot-password-mode.ts │ │ ├── forgot-password.ts │ │ ├── gallery-column.ts │ │ ├── gallery-status.ts │ │ ├── hashtag.ts │ │ ├── health.ts │ │ ├── http-response.ts │ │ ├── identity-token.ts │ │ ├── instance-blocked-domain.ts │ │ ├── instance-statistics.ts │ │ ├── instance.ts │ │ ├── invitation.ts │ │ ├── license.ts │ │ ├── linkable-result.ts │ │ ├── location.ts │ │ ├── login-mode.ts │ │ ├── login.ts │ │ ├── metadata.ts │ │ ├── notification-type.ts │ │ ├── notification.ts │ │ ├── notifications-count.ts │ │ ├── paged-result.ts │ │ ├── profile-page-tab.ts │ │ ├── public-settings.ts │ │ ├── push-subscription.ts │ │ ├── reblog-request.ts │ │ ├── refresh-token.ts │ │ ├── register-mode.ts │ │ ├── register-user.ts │ │ ├── relationship.ts │ │ ├── report-request.ts │ │ ├── report.ts │ │ ├── resend-email-confirmation.ts │ │ ├── reset-password-mode.ts │ │ ├── reset-password.ts │ │ ├── role.ts │ │ ├── rule.ts │ │ ├── search-results.ts │ │ ├── settings.ts │ │ ├── shared-business-card-message.ts │ │ ├── shared-business-card-update-request.ts │ │ ├── shared-business-card.ts │ │ ├── status-comment.ts │ │ ├── status-context.ts │ │ ├── status-request.ts │ │ ├── status-visibility.ts │ │ ├── status.ts │ │ ├── temporary-attachment.ts │ │ ├── trending-period.ts │ │ ├── two-factor-token.ts │ │ ├── upload-photo.ts │ │ ├── user-alias.ts │ │ ├── user-mute-request.ts │ │ ├── user-payload-token.ts │ │ ├── user-setting.ts │ │ ├── user-type.ts │ │ └── user.ts │ ├── pages │ │ ├── account │ │ │ ├── account.page.html │ │ │ ├── account.page.scss │ │ │ └── account.page.ts │ │ ├── article-edit │ │ │ ├── article-edit.page.html │ │ │ ├── article-edit.page.scss │ │ │ └── article-edit.page.ts │ │ ├── articles │ │ │ ├── articles.page.html │ │ │ ├── articles.page.scss │ │ │ └── articles.page.ts │ │ ├── bookmarks │ │ │ ├── bookmarks.page.html │ │ │ ├── bookmarks.page.scss │ │ │ └── bookmarks.page.ts │ │ ├── categories │ │ │ ├── categories.page.html │ │ │ ├── categories.page.scss │ │ │ └── categories.page.ts │ │ ├── category │ │ │ ├── category.page.html │ │ │ ├── category.page.scss │ │ │ └── category.page.ts │ │ ├── confirm-email │ │ │ ├── confirm-email.page.html │ │ │ ├── confirm-email.page.scss │ │ │ └── confirm-email.page.ts │ │ ├── edit-business-card │ │ │ ├── edit-business-card.page.html │ │ │ ├── edit-business-card.page.scss │ │ │ └── edit-business-card.page.ts │ │ ├── editors │ │ │ ├── editors.page.html │ │ │ ├── editors.page.scss │ │ │ └── editors.page.ts │ │ ├── error-items │ │ │ ├── error-items.page.html │ │ │ ├── error-items.page.scss │ │ │ └── error-items.page.ts │ │ ├── errors │ │ │ ├── access-forbidden │ │ │ │ ├── access-forbidden.page.html │ │ │ │ ├── access-forbidden.page.scss │ │ │ │ └── access-forbidden.page.ts │ │ │ ├── connection-lost │ │ │ │ ├── connection-lost.page.html │ │ │ │ ├── connection-lost.page.scss │ │ │ │ └── connection-lost.page.ts │ │ │ ├── page-not-found │ │ │ │ ├── page-not-found.page.html │ │ │ │ ├── page-not-found.page.scss │ │ │ │ └── page-not-found.page.ts │ │ │ └── unexpected-error │ │ │ │ ├── unexpected-error.page.html │ │ │ │ ├── unexpected-error.page.scss │ │ │ │ └── unexpected-error.page.ts │ │ ├── favourites │ │ │ ├── favourites.page.html │ │ │ ├── favourites.page.scss │ │ │ └── favourites.page.ts │ │ ├── forgot-password │ │ │ ├── forgot-password.page.html │ │ │ ├── forgot-password.page.scss │ │ │ └── forgot-password.page.ts │ │ ├── hashtag │ │ │ ├── hashtag.page.html │ │ │ ├── hashtag.page.scss │ │ │ └── hashtag.page.ts │ │ ├── home │ │ │ ├── home.page.html │ │ │ ├── home.page.scss │ │ │ └── home.page.ts │ │ ├── invitations │ │ │ ├── invitations.page.html │ │ │ ├── invitations.page.scss │ │ │ └── invitations.page.ts │ │ ├── login-callback │ │ │ ├── login-callback.page.html │ │ │ ├── login-callback.page.scss │ │ │ └── login-callback.page.ts │ │ ├── login │ │ │ ├── login.page.html │ │ │ ├── login.page.scss │ │ │ └── login.page.ts │ │ ├── news-preview │ │ │ ├── news-preview.page.html │ │ │ ├── news-preview.page.scss │ │ │ └── news-preview.page.ts │ │ ├── news │ │ │ ├── news.page.html │ │ │ ├── news.page.scss │ │ │ └── news.page.ts │ │ ├── notifications │ │ │ ├── notifications.page.html │ │ │ ├── notifications.page.scss │ │ │ └── notifications.page.ts │ │ ├── pages-routing.module.ts │ │ ├── pages.module.ts │ │ ├── preferences │ │ │ ├── preferences.page.html │ │ │ ├── preferences.page.scss │ │ │ └── preferences.page.ts │ │ ├── privacy │ │ │ ├── privacy.page.html │ │ │ ├── privacy.page.scss │ │ │ └── privacy.page.ts │ │ ├── profile │ │ │ ├── profile.page.html │ │ │ ├── profile.page.scss │ │ │ └── profile.page.ts │ │ ├── register │ │ │ ├── register.page.html │ │ │ ├── register.page.scss │ │ │ └── register.page.ts │ │ ├── reports │ │ │ ├── reports.page.html │ │ │ ├── reports.page.scss │ │ │ └── reports.page.ts │ │ ├── reset-password │ │ │ ├── reset-password.page.html │ │ │ ├── reset-password.page.scss │ │ │ └── reset-password.page.ts │ │ ├── search │ │ │ ├── search.page.html │ │ │ ├── search.page.scss │ │ │ └── search.page.ts │ │ ├── settings │ │ │ ├── settings.page.html │ │ │ ├── settings.page.scss │ │ │ └── settings.page.ts │ │ ├── shared-card-public │ │ │ ├── shared-card-public.page.html │ │ │ ├── shared-card-public.page.scss │ │ │ └── shared-card-public.page.ts │ │ ├── shared-card │ │ │ ├── shared-card.page.html │ │ │ ├── shared-card.page.scss │ │ │ └── shared-card.page.ts │ │ ├── shared-cards │ │ │ ├── shared-cards.page.html │ │ │ ├── shared-cards.page.scss │ │ │ └── shared-cards.page.ts │ │ ├── status │ │ │ ├── status.page.html │ │ │ ├── status.page.scss │ │ │ └── status.page.ts │ │ ├── support │ │ │ ├── support.page.html │ │ │ ├── support.page.scss │ │ │ └── support.page.ts │ │ ├── terms │ │ │ ├── terms.page.html │ │ │ ├── terms.page.scss │ │ │ └── terms.page.ts │ │ ├── trending │ │ │ ├── trending.page.html │ │ │ ├── trending.page.scss │ │ │ └── trending.page.ts │ │ ├── upload │ │ │ ├── upload.page.html │ │ │ ├── upload.page.scss │ │ │ ├── upload.page.spec.ts │ │ │ └── upload.page.ts │ │ └── users │ │ │ ├── users.page.html │ │ │ ├── users.page.scss │ │ │ └── users.page.ts │ ├── pipes │ │ ├── ago.pipe.ts │ │ ├── pipes.module.ts │ │ └── sanitize-html.pipe.ts │ ├── services │ │ ├── authorization │ │ │ ├── authorization-guard.service.ts │ │ │ ├── authorization.service.ts │ │ │ └── logged-out-guard.service.ts │ │ ├── common │ │ │ ├── context-statuses.service.ts │ │ │ ├── custom-scripts.service.ts │ │ │ ├── custom-styles.service.ts │ │ │ ├── file-size.service.ts │ │ │ ├── focus-tracker.service.ts │ │ │ ├── loading.service.ts │ │ │ ├── messages.service.ts │ │ │ ├── preferences.service.ts │ │ │ ├── random-generator.service.ts │ │ │ ├── routing-state.service.ts │ │ │ ├── ssr-cookie.service.ts │ │ │ ├── user-display.service.ts │ │ │ ├── web-service-worker.service.ts │ │ │ └── window.service.ts │ │ ├── http │ │ │ ├── account.service.ts │ │ │ ├── archives.service.ts │ │ │ ├── articles.service.ts │ │ │ ├── attachments.service.ts │ │ │ ├── auth-clients.service.ts │ │ │ ├── avatars.service.ts │ │ │ ├── bookmarks.service.ts │ │ │ ├── business-cards.service.ts │ │ │ ├── categories.service.ts │ │ │ ├── countries.service.ts │ │ │ ├── error-items.service.ts │ │ │ ├── exports.service.ts │ │ │ ├── favourites.service.ts │ │ │ ├── follow-requests.service.ts │ │ │ ├── following-imports.service.ts │ │ │ ├── forgot-password.service.ts │ │ │ ├── headers.service.ts │ │ │ ├── health.service.ts │ │ │ ├── identity.service.ts │ │ │ ├── instance-blocked-domains.service.ts │ │ │ ├── instance.service.ts │ │ │ ├── invitations.service.ts │ │ │ ├── liceses.service.ts │ │ │ ├── locations.service.ts │ │ │ ├── notifications.service.ts │ │ │ ├── push-subscriptions.service.ts │ │ │ ├── register.service.ts │ │ │ ├── relationships.service.ts │ │ │ ├── reports.service.ts │ │ │ ├── rules.service.ts │ │ │ ├── search.service.ts │ │ │ ├── settings.service.ts │ │ │ ├── shared-business-cards.service.ts │ │ │ ├── statuses.service.ts │ │ │ ├── timeline.service.ts │ │ │ ├── trending.service.ts │ │ │ ├── user-aliases.service.ts │ │ │ ├── user-settings.service.ts │ │ │ └── users.service.ts │ │ └── persistance │ │ │ └── persistance.service.ts │ └── validators │ │ ├── directives │ │ ├── autocomplete-valid.directive.ts │ │ ├── max-length-validator.directive.ts │ │ ├── password-validator.directive.ts │ │ ├── unique-email-validator.directive.ts │ │ └── unique-user-name-validator.directive.ts │ │ ├── models │ │ └── password-errors.ts │ │ └── validations.module.ts ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── assets │ ├── .gitkeep │ ├── avatar-placeholder.svg │ ├── avatar.afdesign │ ├── avatar.png │ ├── avatar.svg │ ├── beta-dark.png │ ├── beta-light.png │ ├── beta.afdesign │ ├── camera-dark.svg │ ├── camera-light.svg │ ├── camera.afdesign │ ├── face-dark.svg │ ├── face-light.svg │ ├── face.afdesign │ ├── fonts │ │ ├── material-symbols-HRDtGgxmEY.woff2 │ │ └── roboto-12DfhPfYyt.woff2 │ ├── header-placeholder.svg │ ├── header.jpg │ ├── icons │ │ ├── icon-1024x1024.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-dark-rounded.svg │ │ ├── icon-dark.afdesign │ │ ├── icon-dark.svg │ │ ├── icon-light-rounded.svg │ │ ├── icon-light.afdesign │ │ └── icon-light.svg │ ├── logo-dark.afdesign │ ├── logo-dark.svg │ ├── logo-light.afdesign │ ├── logo-light.svg │ ├── news-dark.svg │ ├── news-light.svg │ ├── news.afdesign │ ├── patreon-black.svg │ ├── patreon-white.svg │ ├── user.afdesign │ ├── user.png │ └── user.svg ├── environments │ └── environment.ts ├── favicon.ico ├── index.html ├── main.server.ts ├── main.ts ├── manifest.webmanifest ├── polyfills.ts ├── robots.txt ├── service-worker.js └── styles │ ├── app-theme.scss │ ├── flex-layout.scss │ ├── fonts.scss │ ├── general.scss │ ├── main.scss │ ├── messages-theme.scss │ └── notification-icons.scss ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # flyctl launch added from .gitignore 2 | # See http://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # Compiled output 5 | dist 6 | tmp 7 | out-tsc 8 | bazel-out 9 | 10 | # Node 11 | node_modules 12 | **/npm-debug.log 13 | **/yarn-error.log 14 | 15 | # IDEs and editors 16 | **/.idea 17 | **/.project 18 | **/.classpath 19 | **/.c9 20 | **/*.launch 21 | **/.settings 22 | **/*.sublime-workspace 23 | 24 | # Visual Studio Code 25 | **/.vscode/* 26 | !**/.vscode/settings.json 27 | !**/.vscode/tasks.json 28 | !**/.vscode/launch.json 29 | !**/.vscode/extensions.json 30 | **/.history/* 31 | 32 | # Miscellaneous 33 | .angular/cache 34 | **/.sass-cache 35 | connect.lock 36 | coverage 37 | libpeerconnection.log 38 | **/testem.log 39 | typings 40 | 41 | # System files 42 | **/.DS_Store 43 | **/Thumbs.db 44 | 45 | # flyctl launch added from .idea/.gitignore 46 | # Default ignored files 47 | .idea/shelf 48 | .idea/workspace.xml 49 | # Editor-based HTTP Client requests 50 | .idea/httpRequests 51 | fly.toml 52 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Angular 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - develop 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [20.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Cache node modules 21 | uses: actions/cache@v4 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.os }}-node- 27 | - name: Node ${{ matrix.node-version }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | - name: npm install 32 | run: | 33 | npm install --force 34 | - name: npm run lint 35 | run: | 36 | npm run lint 37 | - name: npm run test 38 | run: | 39 | npm test -- --watch=false --browsers=ChromeHeadless 40 | - name: npm run build 41 | run: | 42 | npm run build 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Stage 1: Compile and Build angular codebase 3 | 4 | # Use official node image as the base image 5 | FROM node:20 as build 6 | 7 | # Set the working directory 8 | WORKDIR /usr/local/app 9 | 10 | # Add the source code to app 11 | COPY ./ /usr/local/app/ 12 | 13 | # Update web build number. 14 | RUN commit=$(git rev-parse --short HEAD) && sed -i -e "s/buildx/$commit/g" src/environments/environment.ts 15 | 16 | # Install all the dependencies 17 | RUN npm install --force 18 | 19 | # Generate the build of the application 20 | RUN npm run build 21 | 22 | ############################################################################### 23 | # Stage 2: Serve dynamic app with node server (SSR). 24 | 25 | # Use official node server. 26 | FROM node:20 AS ssr-server 27 | 28 | # Set the working directory. 29 | WORKDIR /usr/local/app 30 | 31 | # Copy dist files. 32 | COPY --from=build /usr/local/app/dist /usr/local/app/dist/ 33 | 34 | # Copy packages json file. 35 | COPY ./package.json /usr/local/app/package.json 36 | 37 | # Expose port 8080 38 | EXPOSE 8080 39 | 40 | # Run HTTP server. 41 | CMD npm run serve:ssr 42 | -------------------------------------------------------------------------------- /Static.Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Stage 1: Compile and Build angular codebase 3 | 4 | # Use official node image as the base image 5 | FROM node:20 as build 6 | 7 | # Set the working directory 8 | WORKDIR /usr/local/app 9 | 10 | # Add the source code to app 11 | COPY ./ /usr/local/app/ 12 | 13 | # Update web build number. 14 | RUN commit=$(git rev-parse --short HEAD) && sed -i -e "s/buildx/$commit/g" src/app/pages/support/support.page.html 15 | 16 | # Install all the dependencies 17 | RUN npm install --force 18 | 19 | # Generate the build of the application 20 | RUN npm run build:ssr 21 | 22 | ############################################################################### 23 | # Stage 2: Serve static app with nginx server. 24 | 25 | # Use official nginx image as the base image 26 | FROM nginx:latest AS client-browser 27 | 28 | # Use custom ngix file (for rewriting to index.html). 29 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf 30 | 31 | # Copy the build output to replace the default nginx contents. 32 | COPY --from=build /usr/local/app/dist/VernissageWeb/browser/ /usr/share/nginx/html 33 | 34 | # Expose port 8080 35 | EXPOSE 8080 -------------------------------------------------------------------------------- /express.tokens.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | import { Request, Response } from 'express'; 3 | 4 | export const REQUEST = new InjectionToken('REQUEST'); 5 | export const RESPONSE = new InjectionToken('RESPONSE'); -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml app configuration file generated for vernissage-web on 2023-11-09T17:07:07+01:00 2 | # 3 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 4 | # 5 | 6 | app = "vernissage-web" 7 | primary_region = "waw" 8 | 9 | [http_service] 10 | internal_port = 8080 11 | force_https = false 12 | auto_stop_machines = true 13 | auto_start_machines = true 14 | min_machines_running = 1 15 | processes = ["app"] 16 | 17 | [[vm]] 18 | memory = '1gb' 19 | cpu_kind = 'shared' 20 | cpus = 2 -------------------------------------------------------------------------------- /images/01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/images/01.png -------------------------------------------------------------------------------- /images/02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/images/02.png -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen [::]:8080; 3 | 4 | location / { 5 | root /usr/share/nginx/html; 6 | index index.html index.htm; 7 | try_files $uri $uri/ /index.html =404; 8 | } 9 | 10 | # Forward requests to the node container which renders on the server side: 11 | location ~ ^/(public)$ { 12 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 13 | proxy_set_header X-Forwarded-Proto $scheme; 14 | proxy_set_header Host $http_host; 15 | proxy_redirect off; 16 | proxy_pass http://127.0.0.1:8080; 17 | } 18 | 19 | include /etc/nginx/extra-conf.d/*.conf; 20 | } 21 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/service-worker/config/schema.json", 3 | "index": "/index.html", 4 | "assetGroups": [ 5 | { 6 | "name": "app", 7 | "installMode": "prefetch", 8 | "resources": { 9 | "files": [ 10 | "/favicon.ico", 11 | "/index.html", 12 | "/manifest.webmanifest", 13 | "/*.css", 14 | "/*.js" 15 | ] 16 | } 17 | }, 18 | { 19 | "name": "assets", 20 | "installMode": "lazy", 21 | "updateMode": "prefetch", 22 | "resources": { 23 | "files": [ 24 | "/assets/**", 25 | "/*.(svg|cur|jpg|jpeg|png|apng|webp|avif|gif|otf|ttf|woff|woff2)" 26 | ] 27 | } 28 | } 29 | ], 30 | "dataGroups": [ 31 | { 32 | "name": "api-freshness", 33 | "urls": [ 34 | "/api/**" 35 | ], 36 | "cacheConfig": { 37 | "maxSize": 0, 38 | "maxAge": "0u", 39 | "strategy": "freshness" 40 | } 41 | } 42 | ], 43 | "navigationRequestStrategy": "performance" 44 | } 45 | -------------------------------------------------------------------------------- /src/app/animations/fade-in.animation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | trigger, 3 | style, 4 | animate, 5 | transition, 6 | keyframes 7 | } from '@angular/animations'; 8 | 9 | export const fadeInAnimation = [ 10 | trigger('fadeIn', [ 11 | transition(':enter', [ // :enter is alias to 'void => *' 12 | style({ opacity: 0 }), 13 | animate(500, style({ opacity: 1 })) 14 | ]) 15 | ]), 16 | trigger('slowFadeIn', [ 17 | transition(':enter', [ // :enter is alias to 'void => *' 18 | style({ opacity: 0 }), 19 | animate( 20 | 1000, 21 | keyframes([ 22 | style({opacity: 0, offset: 0}), 23 | style({opacity: 0, offset: 0.4}), 24 | style({opacity: 1, offset: 1.0}), 25 | ]), 26 | ) 27 | ]) 28 | ]) 29 | ]; 30 | -------------------------------------------------------------------------------- /src/app/animations/show-or-hide.animation.ts: -------------------------------------------------------------------------------- 1 | import { 2 | trigger, 3 | style, 4 | animate, 5 | transition, 6 | state 7 | } from '@angular/animations'; 8 | 9 | export const showOrHideAnimation = [ 10 | trigger('show', [ 11 | state('false', style({ opacity: 0 })), 12 | state('true', style({ opacity: 1 })), 13 | transition('false => true', animate('500ms')), 14 | ]), 15 | trigger('hide', [ 16 | state('false', style({ opacity: 0 })), 17 | state('true', style({ opacity: 1 })), 18 | transition('true => false', animate('500ms')), 19 | ]), 20 | trigger('showOrHide', [ 21 | state('false', style({ opacity: 0 })), 22 | state('true', style({ opacity: 1 })), 23 | transition('false <=> true', animate('500ms')), 24 | ]), 25 | ] 26 | -------------------------------------------------------------------------------- /src/app/app-initialization.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizationService } from 'src/app/services/authorization/authorization.service'; 2 | import { InstanceService } from 'src/app/services/http/instance.service'; 3 | import { SettingsService } from './services/http/settings.service'; 4 | import { CustomScriptsService } from './services/common/custom-scripts.service'; 5 | import { CustomStylesService } from './services/common/custom-styles.service'; 6 | 7 | export function appInitialization( 8 | authorizationService: AuthorizationService, 9 | instanceService: InstanceService, 10 | settingsService: SettingsService, 11 | customScriptsService: CustomScriptsService, 12 | customStylesService: CustomStylesService 13 | ): any { 14 | return async () => { 15 | try { 16 | await authorizationService.refreshAccessToken(); 17 | 18 | await Promise.all([ 19 | instanceService.load(), 20 | settingsService.load() 21 | ]); 22 | 23 | customScriptsService.injectScripts(); 24 | customStylesService.injectStyles(); 25 | } catch (error) { 26 | console.error(error); 27 | // Suppress error to let global handler navigate to exception page. 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = []; 5 | 6 | @NgModule({ 7 | imports: [RouterModule.forRoot(routes)], 8 | exports: [RouterModule] 9 | }) 10 | export class AppRoutingModule { } 11 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | @if (showLoader()) { 4 |
5 | 6 |
7 | } 8 | 9 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | position: relative; 3 | width: 100%; 4 | top: 64px; 5 | } -------------------------------------------------------------------------------- /src/app/app.module.server.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | 4 | import { AppModule } from './app.module'; 5 | import { AppComponent } from './app.component'; 6 | import { JWT_OPTIONS, JwtModule } from '@auth0/angular-jwt'; 7 | import { WindowService } from './services/common/window.service'; 8 | import { SsrCookieService } from './services/common/ssr-cookie.service'; 9 | 10 | const jwtOptionsFactory = (cookieService: SsrCookieService, windowService: WindowService) => { 11 | return { 12 | tokenGetter: () => { 13 | return cookieService.get('access-token'); 14 | }, 15 | allowedDomains: [windowService.apiService(), 'localhost'] 16 | }; 17 | }; 18 | 19 | @NgModule({ 20 | imports: [ 21 | AppModule, 22 | ServerModule, 23 | JwtModule.forRoot({ 24 | jwtOptionsProvider: { 25 | provide: JWT_OPTIONS, 26 | useFactory: jwtOptionsFactory, 27 | deps: [SsrCookieService, WindowService], 28 | } 29 | }), 30 | ], 31 | bootstrap: [AppComponent], 32 | }) 33 | export class AppServerModule {} 34 | -------------------------------------------------------------------------------- /src/app/common/always-error-state-mather.ts: -------------------------------------------------------------------------------- 1 | import { ErrorStateMatcher } from "@angular/material/core"; 2 | 3 | export class AlwaysErrorStateMatcher implements ErrorStateMatcher { 4 | isErrorState(): boolean { 5 | return true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/common/dirty-error-state-matcher.ts: -------------------------------------------------------------------------------- 1 | import { ErrorStateMatcher } from '@angular/material/core'; 2 | import { FormControl, FormGroupDirective, NgForm } from '@angular/forms'; 3 | 4 | export class DirtyErrorStateMatcher implements ErrorStateMatcher { 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean { 7 | // show error only when dirty and invalid 8 | return control != null && control.dirty && control.invalid; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/components/core/footer/footer.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |
© {{ currentYear() }} {{ apiService() }}
9 |
·
10 |
Powered by Vernissage
11 |
12 |
-------------------------------------------------------------------------------- /src/app/components/core/footer/footer.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/components/core/footer/footer.component.scss -------------------------------------------------------------------------------- /src/app/components/core/footer/footer.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from "@angular/core"; 2 | import { ResponsiveComponent } from "src/app/common/responsive"; 3 | import { WindowService } from "src/app/services/common/window.service"; 4 | 5 | @Component({ 6 | selector: 'app-footer', 7 | templateUrl: './footer.component.html', 8 | styleUrls: ['./footer.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | standalone: false 11 | }) 12 | export class FooterComponent extends ResponsiveComponent implements OnInit { 13 | protected currentYear = signal(''); 14 | protected apiService = signal(''); 15 | 16 | private windowService = inject(WindowService); 17 | 18 | override ngOnInit(): void { 19 | super.ngOnInit(); 20 | 21 | this.currentYear.set(new Date().getFullYear().toString()); 22 | this.apiService.set(this.windowService.apiService()); 23 | } 24 | } -------------------------------------------------------------------------------- /src/app/components/widgets/article-inline/article-inline.component.html: -------------------------------------------------------------------------------- 1 | 2 | @if (article().title; as inlineTitle) { 3 | 4 | {{ inlineTitle }} 5 | 6 | } 7 | 8 | @if (showDismissButton()) { 9 |
10 | 13 |
14 | } 15 |
16 |
17 |
-------------------------------------------------------------------------------- /src/app/components/widgets/article-inline/article-inline.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/components/widgets/article-inline/article-inline.component.scss -------------------------------------------------------------------------------- /src/app/components/widgets/article-inline/article-inline.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, computed, inject, input, output } from '@angular/core'; 2 | import { ResponsiveComponent } from 'src/app/common/responsive'; 3 | import { UserDisplayService } from 'src/app/services/common/user-display.service'; 4 | import { Article } from 'src/app/models/article'; 5 | import { AuthorizationService } from 'src/app/services/authorization/authorization.service'; 6 | 7 | @Component({ 8 | selector: 'app-article-inline', 9 | templateUrl: './article-inline.component.html', 10 | styleUrls: ['./article-inline.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | standalone: false 13 | }) 14 | export class ArticleInlineComponent extends ResponsiveComponent { 15 | public article = input.required
(); 16 | public dismiss = output
(); 17 | 18 | protected showDismissButton = computed(() => { 19 | return !!this.authorizationService.getUser(); 20 | }); 21 | 22 | protected userDisplayService = inject(UserDisplayService); 23 | protected authorizationService = inject(AuthorizationService); 24 | 25 | protected onDismissClick(): void { 26 | this.dismiss.emit(this.article()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/components/widgets/avatar/avatar-size.ts: -------------------------------------------------------------------------------- 1 | export enum AvatarSize { 2 | huge, big, medium, small, verysmall 3 | } -------------------------------------------------------------------------------- /src/app/components/widgets/avatar/avatar.component.html: -------------------------------------------------------------------------------- 1 | Avatar -------------------------------------------------------------------------------- /src/app/components/widgets/avatar/avatar.component.scss: -------------------------------------------------------------------------------- 1 | .rectangle { 2 | border-radius: 15%; 3 | } 4 | 5 | .circle { 6 | border-radius: 50%; 7 | } 8 | 9 | .huge { 10 | width: 48px; 11 | height:48px; 12 | } 13 | 14 | .big { 15 | width: 42px; 16 | height:42px; 17 | } 18 | 19 | .medium { 20 | width: 32px; 21 | height:32px; 22 | } 23 | 24 | .small { 25 | width: 24px; 26 | height:24px; 27 | } 28 | 29 | .verysmall { 30 | width: 16px; 31 | height:16px; 32 | } -------------------------------------------------------------------------------- /src/app/components/widgets/avatar/avatar.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, input, OnInit, signal } from '@angular/core'; 2 | import { User } from 'src/app/models/user'; 3 | import { AvatarSize } from './avatar-size'; 4 | import { PreferencesService } from 'src/app/services/common/preferences.service'; 5 | 6 | @Component({ 7 | selector: 'app-avatar', 8 | templateUrl: './avatar.component.html', 9 | styleUrls: ['./avatar.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | standalone: false 12 | }) 13 | export class AvatarComponent implements OnInit { 14 | public user = input.required(); 15 | public size = input(AvatarSize.huge); 16 | 17 | protected readonly avatarSize = AvatarSize; 18 | protected isCircle = signal(false); 19 | 20 | private preferencesService = inject(PreferencesService); 21 | 22 | ngOnInit(): void { 23 | this.isCircle.set(this.preferencesService.isCircleAvatar); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/components/widgets/category-gallery-item/category-gallery-item.component.scss: -------------------------------------------------------------------------------- 1 | .images-container { 2 | width: 100%; 3 | height: 300px; 4 | overflow-x: auto; 5 | overflow-y: hidden; 6 | font-size: 0.8em; 7 | margin-bottom: 20px; 8 | 9 | .image { 10 | height: 300px; 11 | padding-right: 6px; 12 | } 13 | 14 | .blurhash { 15 | height: 300px; 16 | padding-right: 6px; 17 | } 18 | } 19 | 20 | .hashtag-header { 21 | font-size: 1.5em; 22 | padding-bottom: 20px; 23 | } 24 | 25 | mat-icon { 26 | top: 4px; 27 | position: relative; 28 | transform: scale(1.0); 29 | opacity: 0.8; 30 | } -------------------------------------------------------------------------------- /src/app/components/widgets/category-gallery/category-gallery.component.html: -------------------------------------------------------------------------------- 1 | @if (!isBrowser()) { 2 |
3 | } 4 | @else if (categories().length) { 5 | @for (category of categories(); track category.id) { 6 |
7 | 8 |
9 | } 10 | } @else { 11 |
12 |

13 |
14 |
Sadly, there's nothing to see here just yet.
15 |

16 |
17 | } -------------------------------------------------------------------------------- /src/app/components/widgets/category-gallery/category-gallery.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/components/widgets/category-gallery/category-gallery.component.scss -------------------------------------------------------------------------------- /src/app/components/widgets/category-gallery/category-gallery.component.ts: -------------------------------------------------------------------------------- 1 | import { isPlatformBrowser } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, inject, input, PLATFORM_ID, signal } from '@angular/core'; 3 | import { fadeInAnimation } from 'src/app/animations/fade-in.animation'; 4 | import { ResponsiveComponent } from 'src/app/common/responsive'; 5 | import { Category } from 'src/app/models/category'; 6 | 7 | @Component({ 8 | selector: 'app-category-gallery', 9 | templateUrl: './category-gallery.component.html', 10 | styleUrls: ['./category-gallery.component.scss'], 11 | animations: fadeInAnimation, 12 | changeDetection: ChangeDetectionStrategy.OnPush, 13 | standalone: false 14 | }) 15 | export class CategoryGalleryComponent extends ResponsiveComponent { 16 | public categories = input.required(); 17 | protected isBrowser = signal(false); 18 | 19 | private platformId = inject(PLATFORM_ID); 20 | 21 | constructor() { 22 | super(); 23 | this.isBrowser.set(isPlatformBrowser(this.platformId)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/components/widgets/category-list/category-list.component.scss: -------------------------------------------------------------------------------- 1 | .name { 2 | max-width: 0; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | } 7 | 8 | .actions { 9 | width: 100px; 10 | } 11 | 12 | @media only screen and (min-width: 600px) { 13 | .actions { 14 | width: 210px; 15 | } 16 | } -------------------------------------------------------------------------------- /src/app/components/widgets/comment-reply/comment-reply.component.scss: -------------------------------------------------------------------------------- 1 | .comment-add { 2 | ::ng-deep img { 3 | padding-top: 1px; 4 | } 5 | 6 | button { 7 | margin-top: 4px; 8 | } 9 | 10 | ::ng-deep .mat-mdc-form-field-infix { 11 | width: 100px; 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/components/widgets/domain-blocks/domain-blocks.component.scss: -------------------------------------------------------------------------------- 1 | .domain { 2 | max-width: 0; 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | } 7 | 8 | .actions { 9 | width: 100px; 10 | } 11 | 12 | @media only screen and (min-width: 600px) { 13 | .actions { 14 | width: 210px; 15 | } 16 | } 17 | 18 | @media only screen and (max-width: 599px) { 19 | .mdc-data-table__cell { 20 | padding: 0px; 21 | } 22 | 23 | .mdc-data-table__header-cell { 24 | padding: 0px; 25 | } 26 | } -------------------------------------------------------------------------------- /src/app/components/widgets/follow-buttons-section/follow-buttons-section.component.scss: -------------------------------------------------------------------------------- 1 | .mat-button-toggle-group { 2 | height: 36px; 3 | align-items: center; 4 | } 5 | 6 | .more-button { 7 | padding: 0; 8 | 9 | mat-icon { 10 | margin: 0; 11 | } 12 | } 13 | 14 | .multiple-buttons { 15 | .mat-button-toggle-group { 16 | height: 34px; 17 | align-items: center; 18 | } 19 | } -------------------------------------------------------------------------------- /src/app/components/widgets/gallery/gallery.component.scss: -------------------------------------------------------------------------------- 1 | .gallery-container { 2 | .image-gallery { 3 | display: flex; 4 | flex-direction: row; 5 | gap: 6px; 6 | 7 | .column { 8 | max-width: calc((100vw - 12px) / 3); 9 | } 10 | 11 | &.square { 12 | .column { 13 | ::ng-deep .image-item { 14 | width: calc((100vw - 12px) / 3); 15 | height: calc((100vw - 12px) / 3); 16 | object-fit: cover; 17 | } 18 | } 19 | } 20 | } 21 | 22 | .placeholder { 23 | width: calc((100vw - 12px) / 3); 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/components/widgets/general-settings/general-settings.component.scss: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | 5 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 6 | height: 0; 7 | } 8 | 9 | mat-divider { 10 | margin-bottom: 12px; 11 | } -------------------------------------------------------------------------------- /src/app/components/widgets/hashtag-gallery-item/hashtag-gallery-item.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |
9 | @if (statuses()?.data; as statusesArray) { 10 | @for (status of statusesArray; track status.id) { 11 | 12 | @if (getMainStatus(status).sensitive && !alwaysShowNSFW()) { 13 |
14 | 15 |
16 | } @else { 17 |
18 | 19 |
20 | } 21 |
22 | } 23 | } 24 |
25 |
26 |
-------------------------------------------------------------------------------- /src/app/components/widgets/hashtag-gallery-item/hashtag-gallery-item.component.scss: -------------------------------------------------------------------------------- 1 | .images-container { 2 | width: 100%; 3 | height: 300px; 4 | overflow-x: auto; 5 | overflow-y: hidden; 6 | white-space: nowrap; 7 | margin-bottom: 20px; 8 | 9 | .image { 10 | height: 300px; 11 | padding-right: 6px; 12 | } 13 | 14 | .blurhash { 15 | height: 300px; 16 | padding-right: 6px; 17 | } 18 | } 19 | 20 | .hashtag-header { 21 | font-size: 1.5em; 22 | padding-bottom: 20px; 23 | } 24 | 25 | mat-icon { 26 | top: 4px; 27 | position: relative; 28 | transform: scale(1.4); 29 | opacity: 0.8; 30 | } -------------------------------------------------------------------------------- /src/app/components/widgets/hashtag-gallery/hashtag-gallery.component.html: -------------------------------------------------------------------------------- 1 |
2 | @if (!isBrowser()) { 3 |
4 | } 5 | @else if (hashtags()?.data?.length) { 6 | @for (hashtag of hashtags()?.data; track hashtag.name) { 7 | 8 | } 9 | } @else { 10 |
11 |

12 |
13 |
Sadly, there's nothing to see here just yet.
14 |

15 |
16 | } 17 |
-------------------------------------------------------------------------------- /src/app/components/widgets/hashtag-gallery/hashtag-gallery.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/components/widgets/hashtag-gallery/hashtag-gallery.component.scss -------------------------------------------------------------------------------- /src/app/components/widgets/hashtag-gallery/hashtag-gallery.component.ts: -------------------------------------------------------------------------------- 1 | import { isPlatformBrowser } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, inject, input, PLATFORM_ID, signal } from '@angular/core'; 3 | import { fadeInAnimation } from 'src/app/animations/fade-in.animation'; 4 | import { ResponsiveComponent } from 'src/app/common/responsive'; 5 | import { Hashtag } from 'src/app/models/hashtag'; 6 | import { LinkableResult } from 'src/app/models/linkable-result'; 7 | 8 | @Component({ 9 | selector: 'app-hashtag-gallery', 10 | templateUrl: './hashtag-gallery.component.html', 11 | styleUrls: ['./hashtag-gallery.component.scss'], 12 | animations: fadeInAnimation, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | standalone: false 15 | }) 16 | export class HashtagGalleryComponent extends ResponsiveComponent { 17 | public hashtags = input>(); 18 | protected isBrowser = signal(false); 19 | 20 | private platformId = inject(PLATFORM_ID); 21 | 22 | constructor() { 23 | super(); 24 | this.isBrowser.set(isPlatformBrowser(this.platformId)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/components/widgets/hashtags-search/hashtags-search.component.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |
9 | @for (status of statuses()?.data; track status.id) { 10 | @if (getImageSrc(status)) { 11 |
12 | 13 |
14 | } 15 | } 16 |
17 |
18 |
-------------------------------------------------------------------------------- /src/app/components/widgets/hashtags-search/hashtags-search.component.scss: -------------------------------------------------------------------------------- 1 | .hashtag-header { 2 | font-weight: 600; 3 | padding-bottom: 10px; 4 | } 5 | 6 | mat-icon { 7 | top: 8px; 8 | position: relative; 9 | transform: scale(0.8); 10 | opacity: 0.8; 11 | } 12 | 13 | .image-container { 14 | img { 15 | width: 100px; 16 | height: 100px; 17 | object-fit: cover; 18 | border-radius: 6px; 19 | } 20 | } -------------------------------------------------------------------------------- /src/app/components/widgets/home-signin/home-signin.component.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /src/app/components/widgets/home-signout/home-signout.component.scss: -------------------------------------------------------------------------------- 1 | mat-card { 2 | width: 300px; 3 | border-width: 0; 4 | } 5 | 6 | .header { 7 | text-align: center; 8 | font-size: 2.6em; 9 | margin-top: 40px; 10 | margin-bottom: 30px; 11 | font-weight: 200; 12 | line-height: 1; 13 | } 14 | 15 | .subheader { 16 | text-align: center; 17 | font-size: 1.4em; 18 | margin-top: 20px; 19 | margin-bottom: 30px; 20 | font-weight: 100; 21 | } 22 | 23 | ul { 24 | text-align: center; 25 | list-style: none; 26 | padding: 0; 27 | margin: 30px 10px; 28 | } 29 | 30 | li { 31 | display: inline; 32 | } 33 | 34 | li::after { 35 | content: " "; 36 | word-spacing: 1em; 37 | background-image: linear-gradient( 38 | -0.2turn, 39 | transparent 0 calc(50% - 0.03em), 40 | currentcolor 0 calc(50% + 0.03em), 41 | transparent 0 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/app/components/widgets/instance-rules/instance-rules.component.scss: -------------------------------------------------------------------------------- 1 | .actions { 2 | width: 100px; 3 | } 4 | 5 | @media only screen and (min-width: 600px) { 6 | .actions { 7 | width: 210px; 8 | } 9 | } 10 | 11 | @media only screen and (max-width: 599px) { 12 | .mdc-data-table__cell { 13 | padding: 0px; 14 | } 15 | 16 | .mdc-data-table__header-cell { 17 | padding: 0px; 18 | } 19 | } -------------------------------------------------------------------------------- /src/app/components/widgets/mini-user-card/mini-user-card.component.html: -------------------------------------------------------------------------------- 1 | @if (user(); as userObject) { 2 |
3 |
4 | 5 |
6 |
7 | 10 | @if (showUserName()) { 11 | 12 | } 13 |
14 |
15 | } -------------------------------------------------------------------------------- /src/app/components/widgets/mini-user-card/mini-user-card.component.scss: -------------------------------------------------------------------------------- 1 | 2 | .white { 3 | a { 4 | &:link { 5 | color: rgba(255, 255, 255, 0.6); 6 | text-decoration: none; 7 | } 8 | 9 | &:visited { 10 | color: rgba(255, 255, 255, 0.6); 11 | text-decoration: none; 12 | } 13 | 14 | &:hover { 15 | color: rgba(255, 255, 255, 0.6); 16 | text-decoration: none; 17 | } 18 | 19 | &:active { 20 | color: rgba(255, 255, 255, 0.6); 21 | text-decoration: none; 22 | } 23 | } 24 | } 25 | 26 | .username { 27 | margin-top: -5px; 28 | 29 | .fullname { 30 | line-height: 14px; 31 | 32 | a { 33 | line-height: 14px; 34 | } 35 | } 36 | 37 | .account { 38 | line-height: 14px; 39 | } 40 | } -------------------------------------------------------------------------------- /src/app/components/widgets/mini-user-card/mini-user-card.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; 2 | import { User } from 'src/app/models/user'; 3 | import { AvatarSize } from '../avatar/avatar-size'; 4 | import { UserDisplayService } from 'src/app/services/common/user-display.service'; 5 | 6 | @Component({ 7 | selector: 'app-mini-user-card', 8 | templateUrl: './mini-user-card.component.html', 9 | styleUrls: ['./mini-user-card.component.scss'], 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | standalone: false 12 | }) 13 | export class MiniUserCardComponent { 14 | public user = input.required(); 15 | public size = input(AvatarSize.small); 16 | public showUserName = input(true); 17 | public whiteLink = input(false); 18 | 19 | protected userDisplayService = inject(UserDisplayService); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/components/widgets/password/password.component.scss: -------------------------------------------------------------------------------- 1 | .icons .mat-icon { 2 | line-height: 12px; 3 | } -------------------------------------------------------------------------------- /src/app/components/widgets/password/password.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, signal, output, model, viewChild, input, ChangeDetectionStrategy } from '@angular/core'; 2 | import { NgForm, NgModel } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-password', 6 | templateUrl: './password.component.html', 7 | styleUrls: ['./password.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | standalone: false 10 | }) 11 | export class PasswordComponent { 12 | public passwordText = model(); 13 | public passwordTextChange = output(); 14 | 15 | public form = input(); 16 | public passwordValid = output(); 17 | 18 | protected isPasswordVisible = signal(false); 19 | 20 | private password = viewChild('password'); 21 | 22 | protected togglePassword(): void { 23 | this.isPasswordVisible.update(value => !value); 24 | } 25 | 26 | protected passwordChanged(): void { 27 | this.passwordTextChange.emit(this.passwordText() ?? ''); 28 | this.passwordValid.emit(this.password()?.valid ?? false); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/components/widgets/status-properties/status-properties.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{ publishedAt() | date: 'short' }}
3 | 4 | @switch (status().visibility) { 5 | @case (statusVisibility.Public) { 6 | public 7 | } 8 | @case (statusVisibility.Followers) { 9 | people 10 | } 11 | @case (statusVisibility.Mentioned) { 12 | person 13 | } 14 | } 15 | 16 | @if (status().application) { 17 | settings_applications 18 | } 19 | 20 | rocket 21 |
{{ status().reblogsCount }}
22 | 23 | star 24 |
{{ status().favouritesCount }}
25 |
26 | -------------------------------------------------------------------------------- /src/app/components/widgets/status-properties/status-properties.component.scss: -------------------------------------------------------------------------------- 1 | .properties { 2 | display: flex; 3 | align-items: center; 4 | font-weight: 300; 5 | 6 | mat-icon { 7 | transform: scale(0.75); 8 | } 9 | } -------------------------------------------------------------------------------- /src/app/components/widgets/status-properties/status-properties.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core'; 2 | import { fadeInAnimation } from 'src/app/animations/fade-in.animation'; 3 | import { Status } from 'src/app/models/status'; 4 | import { StatusVisibility } from 'src/app/models/status-visibility'; 5 | 6 | @Component({ 7 | selector: 'app-status-properties', 8 | templateUrl: './status-properties.component.html', 9 | styleUrls: ['./status-properties.component.scss'], 10 | animations: fadeInAnimation, 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | standalone: false 13 | }) 14 | export class StatusPropertiesComponent { 15 | public status = input.required(); 16 | protected readonly statusVisibility = StatusVisibility; 17 | 18 | protected publishedAt = computed(() => { 19 | return this.status().publishedAt ?? this.status().createdAt; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/widgets/statuses-search/statuses-search.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | @if (mainStatus().sensitive && !alwaysShowNSFW) { 5 | 6 | } @else { 7 | 8 | } 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 | 21 | 22 |
23 |
-------------------------------------------------------------------------------- /src/app/components/widgets/statuses-search/statuses-search.component.scss: -------------------------------------------------------------------------------- 1 | hr { 2 | border-top: dashed 1px; 3 | opacity: 0.1; 4 | } -------------------------------------------------------------------------------- /src/app/components/widgets/upload-photo/upload-photo.component.scss: -------------------------------------------------------------------------------- 1 | .exif-label { 2 | width: 140px; 3 | margin-top: 8px; 4 | } 5 | 6 | .exif-values { 7 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 8 | height: 0; 9 | } 10 | } 11 | 12 | .hdr-image { 13 | width: 100px; 14 | 15 | img { 16 | object-fit: cover; 17 | width: 80px; 18 | height: 50px; 19 | } 20 | } 21 | 22 | .uploading-text { 23 | position: relative; 24 | top: -3px; 25 | } 26 | 27 | .double-field { 28 | mat-form-field { 29 | width: 64px; 30 | } 31 | } -------------------------------------------------------------------------------- /src/app/components/widgets/user-card/user-card.component.html: -------------------------------------------------------------------------------- 1 | @if (user(); as userInternal) { 2 |
3 |
4 |
5 | 6 |
7 |
8 |
9 | 10 |
@{{ userInternal.userName }}
11 |
12 |
13 | {{ userInternal.photosCount }} {{ userInternal.photosCount === 1 ? 'Photo' : 'Photos' }} 14 | {{ userInternal.followersCount }} Followers 15 | {{ userInternal.followingCount }} Following 16 |
17 |
18 |
19 |
20 | } -------------------------------------------------------------------------------- /src/app/components/widgets/user-card/user-card.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/components/widgets/user-card/user-card.component.scss -------------------------------------------------------------------------------- /src/app/components/widgets/user-card/user-card.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; 2 | import { ResponsiveComponent } from 'src/app/common/responsive'; 3 | import { User } from 'src/app/models/user'; 4 | import { AvatarSize } from '../avatar/avatar-size'; 5 | import { UserDisplayService } from 'src/app/services/common/user-display.service'; 6 | 7 | @Component({ 8 | selector: 'app-user-card', 9 | templateUrl: './user-card.component.html', 10 | styleUrls: ['./user-card.component.scss'], 11 | changeDetection: ChangeDetectionStrategy.OnPush, 12 | standalone: false 13 | }) 14 | export class UserCardComponent extends ResponsiveComponent { 15 | public user = input(); 16 | protected readonly avatarSize = AvatarSize; 17 | 18 | protected userDisplayService = inject(UserDisplayService); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/components/widgets/user-popover/user-popover.component.scss: -------------------------------------------------------------------------------- 1 | .user-popover { 2 | max-width: 640px; 3 | margin-top: 6px; 4 | 5 | background-color: rgb(255, 255, 255, 0.95); 6 | } 7 | 8 | :host-context(.dark-theme) { 9 | .user-popover { 10 | background-color: rgb(40, 40, 40, 0.95); 11 | } 12 | } -------------------------------------------------------------------------------- /src/app/components/widgets/user-popover/user-popover.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core'; 2 | import { ResponsiveComponent } from 'src/app/common/responsive'; 3 | import { User } from 'src/app/models/user'; 4 | import { AvatarSize } from '../avatar/avatar-size'; 5 | import { UserDisplayService } from 'src/app/services/common/user-display.service'; 6 | import { Relationship } from 'src/app/models/relationship'; 7 | 8 | @Component({ 9 | selector: 'app-user-popover', 10 | templateUrl: './user-popover.component.html', 11 | styleUrls: ['./user-popover.component.scss'], 12 | changeDetection: ChangeDetectionStrategy.OnPush, 13 | standalone: false 14 | }) 15 | export class UserPopoverComponent extends ResponsiveComponent { 16 | public user = input.required(); 17 | public relationship = input(); 18 | 19 | protected readonly avatarSize = AvatarSize; 20 | protected userDisplayService = inject(UserDisplayService); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/components/widgets/user-selector/user-selector.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/components/widgets/user-selector/user-selector.component.scss -------------------------------------------------------------------------------- /src/app/components/widgets/users-card/users-card.component.scss: -------------------------------------------------------------------------------- 1 | .user-wrapper { 2 | align-items: center; 3 | display: flex; 4 | gap: 10px; 5 | padding-bottom: 12px; 6 | padding-top: 12px; 7 | 8 | .user-display-name { 9 | display: flex; 10 | gap: 10px; 11 | flex: 1 1 auto; 12 | overflow: hidden; 13 | 14 | .user-avatar { 15 | margin-top: 4px; 16 | } 17 | } 18 | 19 | .user-name { 20 | display: inline-block; 21 | } 22 | 23 | .verification-badge { 24 | display: inline-block; 25 | padding-left: 10px; 26 | text-wrap: nowrap; 27 | 28 | .verification-icon { 29 | width: 14px; 30 | height: 14px; 31 | } 32 | } 33 | 34 | .user-statuses-tag { 35 | display: none; 36 | } 37 | 38 | @media only screen and (min-width: 500px) { 39 | .user-statuses-tag { 40 | display: block; 41 | } 42 | } 43 | 44 | .user-following-tag { 45 | display: none; 46 | } 47 | 48 | @media only screen and (min-width: 600px) { 49 | .user-following-tag { 50 | display: block; 51 | } 52 | } 53 | 54 | .bio { 55 | opacity: 0.7; 56 | 57 | ::ng-deep p { 58 | margin-top: 4px; 59 | margin-bottom: 4px; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/app/components/widgets/users-gallery-item/users-gallery-item.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | @if (statuses(); as statusesArray) { 6 | @if (statusesArray.data) { 7 | @for (status of statusesArray.data; track status.id) { 8 | 9 | @if (getMainStatus(status).sensitive && !alwaysShowNSFW()) { 10 |
11 | 12 |
13 | } @else { 14 |
15 | 16 |
17 | } 18 |
19 | } 20 | } 21 | } 22 |
23 |
24 |
-------------------------------------------------------------------------------- /src/app/components/widgets/users-gallery-item/users-gallery-item.component.scss: -------------------------------------------------------------------------------- 1 | .images-container { 2 | width: 100%; 3 | height: 300px; 4 | overflow-x: auto; 5 | overflow-y: hidden; 6 | white-space: nowrap; 7 | margin-bottom: 20px; 8 | 9 | .image { 10 | height: 300px; 11 | padding-right: 6px; 12 | } 13 | 14 | .blurhash { 15 | height: 300px; 16 | padding-right: 6px; 17 | } 18 | } -------------------------------------------------------------------------------- /src/app/components/widgets/users-gallery/users-gallery.component.html: -------------------------------------------------------------------------------- 1 |
2 | @if (!isBrowser()) { 3 |
4 | } 5 | @else if (users()?.data?.length) { 6 | @for (user of users()?.data; track user.userName) { 7 |
8 | 9 |
10 | } 11 | } @else { 12 |
13 |

14 |
15 |
Sadly, there's nothing to see here just yet.
16 |

17 |
18 | } 19 |
-------------------------------------------------------------------------------- /src/app/components/widgets/users-gallery/users-gallery.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/components/widgets/users-gallery/users-gallery.component.scss -------------------------------------------------------------------------------- /src/app/components/widgets/users-gallery/users-gallery.component.ts: -------------------------------------------------------------------------------- 1 | import { isPlatformBrowser } from '@angular/common'; 2 | import { ChangeDetectionStrategy, Component, inject, input, PLATFORM_ID, signal } from '@angular/core'; 3 | import { fadeInAnimation } from 'src/app/animations/fade-in.animation'; 4 | import { ResponsiveComponent } from 'src/app/common/responsive'; 5 | import { LinkableResult } from 'src/app/models/linkable-result'; 6 | import { User } from 'src/app/models/user'; 7 | 8 | @Component({ 9 | selector: 'app-users-gallery', 10 | templateUrl: './users-gallery.component.html', 11 | styleUrls: ['./users-gallery.component.scss'], 12 | animations: fadeInAnimation, 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | standalone: false 15 | }) 16 | export class UsersGalleryComponent extends ResponsiveComponent { 17 | public users = input>(); 18 | protected isBrowser = signal(false); 19 | 20 | private platformId = inject(PLATFORM_ID); 21 | 22 | constructor() { 23 | super(); 24 | this.isBrowser.set(isPlatformBrowser(this.platformId)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/dialogs/category-dialog/category-hashtag-item.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Name 4 | 5 | Enter hashtag name. 6 | Name is too long. 7 | 10 | 11 |
-------------------------------------------------------------------------------- /src/app/dialogs/category-dialog/category-hashtag-item.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/dialogs/category-dialog/category-hashtag-item.component.scss -------------------------------------------------------------------------------- /src/app/dialogs/category-dialog/category-hashtag-item.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, input, output } from "@angular/core"; 2 | import { ControlContainer, NgForm } from "@angular/forms"; 3 | import { CategoryHashtag } from "src/app/models/category-hashtag"; 4 | 5 | @Component({ 6 | selector: 'app-category-hashtag-item', 7 | templateUrl: 'category-hashtag-item.component.html', 8 | styleUrls: ['category-hashtag-item.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | viewProviders: [{ provide: ControlContainer, useExisting: NgForm }], 11 | standalone: false 12 | }) 13 | export class CategoryHashtagItemComponent { 14 | public hashtag = input.required(); 15 | public index = input.required(); 16 | public delete = output(); 17 | 18 | protected onHashtagNameChange(name: string): void { 19 | this.hashtag().hashtag = name; 20 | } 21 | 22 | protected onDelete(): void { 23 | this.delete.emit(this.hashtag()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/dialogs/category-dialog/category.dialog.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 2 | height: 16px; 3 | } -------------------------------------------------------------------------------- /src/app/dialogs/change-password-dialog/change-password.dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
Change password
3 |
4 | 5 |
6 | 7 | Old password 8 | 9 | Enter password. 10 | Password is too long. 11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 |
-------------------------------------------------------------------------------- /src/app/dialogs/confirmation-dialog/confirmation.dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | {{ text() }} 5 |
6 |
7 |
8 | 9 | 10 |
11 |
-------------------------------------------------------------------------------- /src/app/dialogs/confirmation-dialog/confirmation.dialog.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/dialogs/confirmation-dialog/confirmation.dialog.scss -------------------------------------------------------------------------------- /src/app/dialogs/confirmation-dialog/confirmation.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-confirmation-dialog', 6 | templateUrl: 'confirmation.dialog.html', 7 | styleUrls: ['confirmation.dialog.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | standalone: false 10 | }) 11 | export class ConfirmationDialog implements OnInit { 12 | protected text = signal(''); 13 | 14 | private dialogRef = inject(MatDialogRef); 15 | private data?: string = inject(MAT_DIALOG_DATA); 16 | 17 | ngOnInit(): void { 18 | this.text.set(this.data ?? ''); 19 | } 20 | 21 | protected onNoClick(): void { 22 | this.dialogRef.close({ confirmed: false}); 23 | } 24 | 25 | protected async onSubmit(): Promise { 26 | this.dialogRef.close({ confirmed: true}); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/dialogs/content-warning-dialog/content-warning.dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
Content warning
3 |
4 |
5 |
6 | Enter new content warning for the status. 7 |
8 | 9 | Content warning 10 | 11 | 12 | Token is required. 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 |
-------------------------------------------------------------------------------- /src/app/dialogs/content-warning-dialog/content-warning.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, model } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-content-warning-dialog', 6 | templateUrl: 'content-warning.dialog.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | standalone: false 9 | }) 10 | export class ContentWarningDialog { 11 | protected contentWarning = model(''); 12 | 13 | private dialogRef = inject(MatDialogRef); 14 | private data?: string = inject(MAT_DIALOG_DATA); 15 | 16 | protected onNoClick(): void { 17 | this.dialogRef.close(); 18 | } 19 | 20 | protected async onSubmit(): Promise { 21 | this.dialogRef.close({ contentWarning: this.contentWarning(), statusId: this.data }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/dialogs/create-alias-dialog/create-alias.dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
Create account alias
3 |
4 | 5 | 6 | Specify the username@domain of the account you want to move from. 7 | 8 | 9 |
10 | 11 | Account alias 12 | 13 | Account alias is too long. 14 | Enter account alias. 15 | 16 |
17 | 18 |
19 |
20 | 21 | 22 |
23 |
-------------------------------------------------------------------------------- /src/app/dialogs/create-alias-dialog/create-alias.dialog.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 2 | height: 16px; 3 | } -------------------------------------------------------------------------------- /src/app/dialogs/delete-account-dialog/delete-account.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, model } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { User } from 'src/app/models/user'; 4 | 5 | @Component({ 6 | selector: 'app-delete-account-dialog', 7 | templateUrl: 'delete-account.dialog.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | standalone: false 10 | }) 11 | export class DeleteAccountDialog { 12 | protected email = model(''); 13 | 14 | public dialogRef = inject(MatDialogRef) 15 | public data: User = inject(MAT_DIALOG_DATA); 16 | 17 | protected onNoClick(): void { 18 | this.dialogRef.close(); 19 | } 20 | 21 | protected async onSubmit(): Promise { 22 | this.dialogRef.close({ confirmed: true}); 23 | } 24 | } -------------------------------------------------------------------------------- /src/app/dialogs/delete-status-dialog/delete-status.dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
Delete status?
3 |
4 |
5 | Do you want to delete status? 6 |
7 |
8 |
9 | 10 | 11 |
12 |
-------------------------------------------------------------------------------- /src/app/dialogs/delete-status-dialog/delete-status.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material/dialog'; 3 | 4 | @Component({ 5 | selector: 'app-delete-status-dialog', 6 | templateUrl: 'delete-status.dialog.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush, 8 | standalone: false 9 | }) 10 | export class DeleteStatusDialog { 11 | private dialogRef = inject(MatDialogRef); 12 | 13 | protected onNoClick(): void { 14 | this.dialogRef.close(); 15 | } 16 | 17 | protected async onSubmit(): Promise { 18 | this.dialogRef.close({ confirmation: true }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/dialogs/disable-two-factor-token/disable-two-factor-token.dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
Disable two factor authentication (2FA)
3 |
4 |
5 |
6 | Enter token from 2FA application (like Authy or Google Authenticator). 7 |
8 | 9 | Token 10 | 11 | 12 | Token is required. 13 | 14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 |
-------------------------------------------------------------------------------- /src/app/dialogs/disable-two-factor-token/disable-two-factor-token.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, model } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material/dialog'; 3 | import { MessagesService } from 'src/app/services/common/messages.service'; 4 | import { AccountService } from 'src/app/services/http/account.service'; 5 | 6 | @Component({ 7 | selector: 'app-disable-two-factor-token-dialog', 8 | templateUrl: 'disable-two-factor-token.dialog.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | standalone: false 11 | }) 12 | export class DisableTwoFactorTokenDialog { 13 | protected code = model(''); 14 | 15 | private accountService = inject(AccountService); 16 | private messageService = inject(MessagesService); 17 | private dialogRef = inject(MatDialogRef); 18 | 19 | protected onNoClick(): void { 20 | this.dialogRef.close(); 21 | } 22 | 23 | protected async onSubmit(): Promise { 24 | try { 25 | await this.accountService.disableTwoFactorToken(this.code()); 26 | this.messageService.showSuccess('Two factor authentication disabled.'); 27 | this.dialogRef.close({}); 28 | } catch (error) { 29 | console.error(error); 30 | this.messageService.showServerError(error); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/dialogs/enable-two-factor-token/enable-two-factor-token.dialog.scss: -------------------------------------------------------------------------------- 1 | .token { 2 | width: 160px; 3 | } 4 | 5 | ::ng-deep .qr-code { 6 | svg { 7 | border-radius: 14px; 8 | } 9 | } -------------------------------------------------------------------------------- /src/app/dialogs/error-item-dialog/error-item.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { ErrorItem } from 'src/app/models/error-item'; 4 | 5 | @Component({ 6 | selector: 'app-error-item-dialog', 7 | templateUrl: 'error-item.dialog.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | standalone: false 10 | }) 11 | export class ErrorItemDialog implements OnInit { 12 | protected code = signal(''); 13 | protected message = signal(''); 14 | protected exception = signal(''); 15 | 16 | private dialogRef = inject(MatDialogRef); 17 | private data?: ErrorItem = inject(MAT_DIALOG_DATA); 18 | 19 | ngOnInit(): void { 20 | this.code.set(this.data?.code ?? ''); 21 | this.message.set(this.data?.message ?? ''); 22 | this.exception.set(this.data?.exception ?? ''); 23 | } 24 | 25 | protected onNoClick(): void { 26 | this.dialogRef.close(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/dialogs/following-import-accounts-dialog/following-import-accounts.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { FollowingImport } from 'src/app/models/following-import'; 4 | import { FollowingImportItemStatus } from 'src/app/models/following-import-item-status'; 5 | 6 | @Component({ 7 | selector: 'app-following-import-accounts-dialog', 8 | templateUrl: 'following-import-accounts.dialog.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | standalone: false 11 | }) 12 | export class FollowingImportAccountsDialog { 13 | protected readonly followingImportItemStatus = FollowingImportItemStatus; 14 | protected readonly followingImportsDisplayedColumns: string[] = ['account', 'startedAt', 'endedAt', 'status']; 15 | 16 | protected data?: FollowingImport = inject(MAT_DIALOG_DATA); 17 | private dialogRef = inject(MatDialogRef) 18 | 19 | protected onNoClick(): void { 20 | this.dialogRef.close(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/dialogs/instance-blocked-domain-dialog/instance-blocked-domain.dialog.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 2 | height: 16px; 3 | } -------------------------------------------------------------------------------- /src/app/dialogs/instance-rule-dialog/instance-rule.dialog.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 2 | height: 16px; 3 | } -------------------------------------------------------------------------------- /src/app/dialogs/mute-account-dialog/mute-account.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, model } from '@angular/core'; 2 | import { MatDialogRef } from '@angular/material/dialog'; 3 | import { UserMuteRequest } from 'src/app/models/user-mute-request'; 4 | 5 | @Component({ 6 | selector: 'app-mute-account-dialog', 7 | templateUrl: 'mute-account.dialog.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | standalone: false 10 | }) 11 | export class MuteAccountDialog { 12 | protected muteStatuses = model(false); 13 | protected muteReblogs = model(false); 14 | protected muteNotifications = model(false); 15 | protected muteEnd = model(); 16 | 17 | public dialogRef = inject(MatDialogRef); 18 | 19 | protected onNoClick(): void { 20 | this.dialogRef.close(); 21 | } 22 | 23 | protected async onSubmit(): Promise { 24 | const userMuteRequest = new UserMuteRequest(this.muteStatuses(), this.muteReblogs(), this.muteNotifications(), this.muteEnd()); 25 | this.dialogRef.close(userMuteRequest); 26 | } 27 | } -------------------------------------------------------------------------------- /src/app/dialogs/notification-settings-dialog/notification-settings.dialog.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/dialogs/notification-settings-dialog/notification-settings.dialog.scss -------------------------------------------------------------------------------- /src/app/dialogs/profile-code-dialog/profile-code.dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
{{ profileUrl() }}
5 |
6 |
-------------------------------------------------------------------------------- /src/app/dialogs/profile-code-dialog/profile-code.dialog.scss: -------------------------------------------------------------------------------- 1 | .mat-mdc-dialog-content { 2 | padding: 0; 3 | } 4 | 5 | ::ng-deep .mat-mdc-dialog-surface { 6 | border-radius: 14px; 7 | } 8 | 9 | .profileUrl { 10 | padding: 10px; 11 | width: 260px; 12 | overflow-wrap: break-word; 13 | font-size: 0.7em; 14 | line-height: 12px; 15 | text-align: center; 16 | } -------------------------------------------------------------------------------- /src/app/dialogs/report-dialog/report-data.ts: -------------------------------------------------------------------------------- 1 | import { Status } from "src/app/models/status"; 2 | import { User } from "src/app/models/user"; 3 | 4 | export class ReportData { 5 | public user?: User; 6 | public status?: Status; 7 | 8 | constructor(user?: User, status?: Status) { 9 | this.user = user; 10 | this.status = status; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/dialogs/share-business-card-dialog/share-business-card.dialog.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, model, OnInit } from '@angular/core'; 2 | import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; 3 | import { SharedBusinessCard } from 'src/app/models/shared-business-card'; 4 | 5 | @Component({ 6 | selector: 'app-share-business-card-dialog', 7 | templateUrl: 'share-business-card.dialog.html', 8 | changeDetection: ChangeDetectionStrategy.OnPush, 9 | standalone: false 10 | }) 11 | export class ShareBusinessCardDialog implements OnInit { 12 | protected title = model(''); 13 | protected note = model(''); 14 | 15 | private dialogRef = inject(MatDialogRef); 16 | private data?: SharedBusinessCard = inject(MAT_DIALOG_DATA); 17 | 18 | ngOnInit(): void { 19 | this.title.set(this.data?.title ?? ''); 20 | this.note.set(this.data?.note ?? ''); 21 | } 22 | 23 | protected onNoClick(): void { 24 | this.dialogRef.close(); 25 | } 26 | 27 | protected async onSubmit(): Promise { 28 | const sharedBusinessCard = this.data ?? new SharedBusinessCard(); 29 | sharedBusinessCard.title = this.title(); 30 | sharedBusinessCard.note = this.note(); 31 | 32 | this.dialogRef.close(sharedBusinessCard); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/dialogs/status-text-template-dialog/status-text-template.dialog.html: -------------------------------------------------------------------------------- 1 |
2 |
Status text template
3 |
4 | 5 |
6 | 7 | Template 8 | 11 | Too long template. 12 | 13 |
14 |
15 |
16 | 17 | 18 |
19 |
-------------------------------------------------------------------------------- /src/app/dialogs/user-roles-dialog/user-roles.dialog.html: -------------------------------------------------------------------------------- 1 |
User roles
2 |
3 | 4 |
5 | 6 | Administrator 7 | 8 | 9 | Moderator 10 | 11 | 12 | Member 13 | 14 |
15 |
16 |
17 | 18 |
19 | -------------------------------------------------------------------------------- /src/app/dialogs/users-dialog/users-dialog-context.ts: -------------------------------------------------------------------------------- 1 | export enum UsersListType { 2 | reblogged, 3 | favourited 4 | } 5 | 6 | export class UsersDialogContext { 7 | public statusId: string; 8 | public usersListType: UsersListType; 9 | public title: string; 10 | 11 | constructor(statusId: string, usersListType: UsersListType, title: string) { 12 | this.statusId = statusId; 13 | this.usersListType = usersListType; 14 | this.title = title; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/dialogs/users-dialog/users.dialog.html: -------------------------------------------------------------------------------- 1 |
{{ title() }}
2 |
3 | 4 |
5 | @if ((users()?.data?.length ?? 0) > 0) { 6 | 11 | 12 | } @else { 13 |

14 |
15 |
Sadly, there are no users to display just yet.
16 |

17 | } 18 |
19 |
20 |
21 | 22 |
23 | -------------------------------------------------------------------------------- /src/app/dialogs/users-dialog/users.dialog.scss: -------------------------------------------------------------------------------- 1 | // Fix the size of fonts in the dialog content. 2 | ::ng-deep .mat-mdc-dialog-container .mdc-dialog__content { 3 | color: inherit !important; 4 | } 5 | 6 | ::ng-deep .mat-mdc-dialog-container .mdc-dialog__content { 7 | line-height: inherit !important; 8 | font-size: inherit !important; 9 | font-weight: inherit !important; 10 | letter-spacing: inherit !important; 11 | } -------------------------------------------------------------------------------- /src/app/directives/directive.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { LazyLoadDirective } from './lazy-load.directive'; 6 | import { InfiniteScrollDirective } from './infinite-scroll.directive'; 7 | import { NoteProcessorDirective } from './note-processor.directive'; 8 | import { HrefToRouterLinkDirective } from './href-to-router-link.directive'; 9 | import { InputActivityDirective } from './input-activity.directive'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | LazyLoadDirective, 14 | InfiniteScrollDirective, 15 | NoteProcessorDirective, 16 | HrefToRouterLinkDirective, 17 | InputActivityDirective 18 | ], 19 | imports: [ 20 | BrowserModule, 21 | BrowserAnimationsModule, 22 | FormsModule 23 | ], 24 | exports: [ 25 | LazyLoadDirective, 26 | InfiniteScrollDirective, 27 | NoteProcessorDirective, 28 | HrefToRouterLinkDirective, 29 | InputActivityDirective 30 | ] 31 | }) 32 | export class DirectivesModule { } 33 | -------------------------------------------------------------------------------- /src/app/directives/input-activity.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, HostListener, inject } from '@angular/core'; 2 | import { FocusTrackerService } from '../services/common/focus-tracker.service'; 3 | 4 | @Directive({ 5 | // eslint-disable-next-line @angular-eslint/directive-selector 6 | selector: ` 7 | input[type=text], 8 | input[type=email], 9 | input[type=number], 10 | input[type=password], 11 | input[type=search], 12 | input[type=tel], 13 | input[type=url], 14 | textarea, 15 | [contenteditable="true"] 16 | `, 17 | standalone: false 18 | }) 19 | export class InputActivityDirective { 20 | private focusTrackerService = inject(FocusTrackerService); 21 | 22 | @HostListener('focus') 23 | onFocus() { 24 | this.focusTrackerService.setFocusState(true); 25 | } 26 | 27 | @HostListener('blur') 28 | onBlur() { 29 | this.focusTrackerService.setFocusState(false); 30 | } 31 | } -------------------------------------------------------------------------------- /src/app/errors/custom-error.ts: -------------------------------------------------------------------------------- 1 | export class CustomError { 2 | message: string; 3 | 4 | constructor(message: string) { 5 | this.message = message; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/errors/forbidden-error.ts: -------------------------------------------------------------------------------- 1 | export class ForbiddenError { 2 | } 3 | -------------------------------------------------------------------------------- /src/app/errors/object-not-found-error.ts: -------------------------------------------------------------------------------- 1 | export class ObjectNotFoundError { 2 | constructor(public objectId: any) { 3 | this.objectId = objectId; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/app/errors/page-not-found-error.ts: -------------------------------------------------------------------------------- 1 | export class PageNotFoundError { 2 | } 3 | -------------------------------------------------------------------------------- /src/app/errors/server-refresh-token-not-exists-error.ts: -------------------------------------------------------------------------------- 1 | export class ServerRefreshTokenNotExistsError { 2 | } 3 | -------------------------------------------------------------------------------- /src/app/models/account-mode.ts: -------------------------------------------------------------------------------- 1 | export enum AccountMode { 2 | Account, 3 | Submitting, 4 | Success, 5 | Error 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/archive-status.ts: -------------------------------------------------------------------------------- 1 | export enum ArchiveStatus { 2 | New = 'new', 3 | Processing = 'processing', 4 | Ready = 'ready', 5 | Expired = 'expired', 6 | Error = 'error' 7 | } -------------------------------------------------------------------------------- /src/app/models/archive.ts: -------------------------------------------------------------------------------- 1 | import { ArchiveStatus } from "./archive-status"; 2 | import { User } from "./user"; 3 | 4 | export class Archive { 5 | public id = ''; 6 | public user?: User; 7 | public requestDate?: Date; 8 | public startDate?: Date; 9 | public endDate?: Date; 10 | public fileName?: string; 11 | public status?: ArchiveStatus; 12 | public errorMessage?: string; 13 | public createdAt?: Date; 14 | public updatedAt?: Date; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/models/article-file-info.ts: -------------------------------------------------------------------------------- 1 | export class ArticleFileInfo { 2 | public url = ''; 3 | public width = 0; 4 | public height = 0; 5 | public aspect = 0; 6 | } -------------------------------------------------------------------------------- /src/app/models/article-visibility.ts: -------------------------------------------------------------------------------- 1 | export enum ArticleVisibility { 2 | SignOutHome = 'signOutHome', 3 | SignInHome = 'signInHome', 4 | SignInNews = 'signInNews', 5 | SignOutNews = 'signOutNews' 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/article.ts: -------------------------------------------------------------------------------- 1 | import { ArticleFileInfo } from "./article-file-info"; 2 | import { ArticleVisibility } from "./article-visibility"; 3 | import { User } from "./user"; 4 | 5 | export class Article { 6 | public id?: string; 7 | public title?: string; 8 | public body = ''; 9 | public bodyHtml?: string; 10 | public color?: string; 11 | public alternativeAuthor?: string; 12 | public user?: User; 13 | public mainArticleFileInfo?: ArticleFileInfo; 14 | public createdAt?: Date; 15 | public updatedAt?: Date; 16 | public visibilities?: ArticleVisibility[]; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/models/attachment-description.ts: -------------------------------------------------------------------------------- 1 | export class AttachmentDescription { 2 | public description?: string; 3 | } -------------------------------------------------------------------------------- /src/app/models/attachment-hashtag.ts: -------------------------------------------------------------------------------- 1 | export class AttachmentHashtag { 2 | public hashtags?: string[]; 3 | } -------------------------------------------------------------------------------- /src/app/models/attachment.ts: -------------------------------------------------------------------------------- 1 | import { FileInfo } from './file-info'; 2 | import { Metadata } from './metadata'; 3 | import { Location } from './location'; 4 | import { License } from './license'; 5 | 6 | export class Attachment { 7 | public id = ''; 8 | public originalFile?: FileInfo; 9 | public smallFile?: FileInfo; 10 | public originalHdrFile?: FileInfo; 11 | public description?: string; 12 | public blurhash?: string; 13 | public metadata?: Metadata; 14 | public location?: Location; 15 | public license?: License; 16 | } -------------------------------------------------------------------------------- /src/app/models/auth-client.ts: -------------------------------------------------------------------------------- 1 | export class AuthClient { 2 | public id?: string; 3 | public uri?: string; 4 | public name?: string; 5 | public svgIcon?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/boolean-result.ts: -------------------------------------------------------------------------------- 1 | export class BooleanResult { 2 | public result = false; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/models/business-card-avatar.ts: -------------------------------------------------------------------------------- 1 | export class BusinessCardAvatar { 2 | public file = ''; 3 | public type = ''; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/models/business-card-field.ts: -------------------------------------------------------------------------------- 1 | export class BusinessCardField { 2 | public id?: string; 3 | public key = ''; 4 | public value = ''; 5 | 6 | constructor(id: string | undefined, key: string, value: string) { 7 | this.id = id; 8 | this.key = key; 9 | this.value = value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/models/business-card.ts: -------------------------------------------------------------------------------- 1 | import { BusinessCardField } from "./business-card-field"; 2 | import { User } from "./user"; 3 | 4 | export class BusinessCard { 5 | public id?: string; 6 | public user?: User; 7 | public title = ''; 8 | public subtitle?: string; 9 | public body?: string; 10 | public website?: string; 11 | public telephone?: string; 12 | public email?: string; 13 | public color1 = '#ad5389'; 14 | public color2 = '#3c1053'; 15 | public color3 = '#ffffff'; 16 | 17 | public fields?: BusinessCardField[]; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/models/category-hashtag.ts: -------------------------------------------------------------------------------- 1 | export class CategoryHashtag { 2 | public id?: string; 3 | public hashtag = ''; 4 | public hashtagNormalized = ''; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/category-statuses.ts: -------------------------------------------------------------------------------- 1 | import { LinkableResult } from "./linkable-result"; 2 | import { Status } from "./status"; 3 | 4 | export class CategoryStatuses { 5 | public id: string; 6 | public name: string; 7 | public statuses: LinkableResult; 8 | 9 | constructor(id: string, name: string, statuses: LinkableResult) { 10 | this.id = id; 11 | this.name = name; 12 | this.statuses = statuses; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/models/category.ts: -------------------------------------------------------------------------------- 1 | import { CategoryHashtag } from "./category-hashtag"; 2 | 3 | export class Category { 4 | public id?: string; 5 | public name = ''; 6 | public priority = 0; 7 | public hashtags?: CategoryHashtag[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/models/change-email.ts: -------------------------------------------------------------------------------- 1 | export class ChangeEmail { 2 | public email?: string; 3 | public redirectBaseUrl?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/models/change-password.ts: -------------------------------------------------------------------------------- 1 | export class ChangePassword { 2 | public currentPassword: string; 3 | public newPassword: string; 4 | 5 | constructor(currentPassword = '', newPassword = '') { 6 | this.currentPassword = currentPassword; 7 | this.newPassword = newPassword; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/models/configuration-attachments.ts: -------------------------------------------------------------------------------- 1 | export class ConfigurationAttachments { 2 | public supportedMimeTypes: string[] = []; 3 | public imageSizeLimit = 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/models/configuration-statuses.ts: -------------------------------------------------------------------------------- 1 | export class ConfigurationStatuses { 2 | public maxCharacters = 0; 3 | public maxMediaAttachments = 0; 4 | public charactersReservedPerUrl = 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/configuration.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationAttachments } from 'src/app/models/configuration-attachments'; 2 | import { ConfigurationStatuses } from 'src/app/models/configuration-statuses'; 3 | 4 | export class Configuration { 5 | public statuses?: ConfigurationStatuses; 6 | public attachments?: ConfigurationAttachments; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/models/confirm-email-mode.ts: -------------------------------------------------------------------------------- 1 | export enum ConfirmEmailMode { 2 | Validating, 3 | Success, 4 | Error 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/confirm-email.ts: -------------------------------------------------------------------------------- 1 | export class ConfirmEmail { 2 | public id: string; 3 | public confirmationGuid: string; 4 | 5 | constructor(id: string, confirmationGuid: string) { 6 | this.id = id; 7 | this.confirmationGuid = confirmationGuid; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/models/content-warning.ts: -------------------------------------------------------------------------------- 1 | export class ContentWarning { 2 | public contentWarning: string; 3 | 4 | constructor(contentWarning: string) { 5 | this.contentWarning = contentWarning; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/models/context-timeline.ts: -------------------------------------------------------------------------------- 1 | export enum ContextTimeline { 2 | unknown, 3 | home, 4 | local, 5 | global, 6 | trendingStatusesDaily, 7 | trendingStatusesMonthly, 8 | trendingStatusesYearly, 9 | editors, 10 | category, 11 | hashtag, 12 | user, 13 | bookmarks, 14 | favourites 15 | } 16 | -------------------------------------------------------------------------------- /src/app/models/country.ts: -------------------------------------------------------------------------------- 1 | export class Country { 2 | public id?: string; 3 | public name?: string; 4 | public code?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/email-secure-method.ts: -------------------------------------------------------------------------------- 1 | export enum EmailSecureMethod { 2 | None = 'none', 3 | Ssl = 'ssl', 4 | StartTls = 'startTls', 5 | StartTlsWhenAvailable = 'startTlsWhenAvailable' 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/error-item-source.ts: -------------------------------------------------------------------------------- 1 | export enum ErrorItemSource { 2 | Client = 'client', 3 | Server = 'server' 4 | } -------------------------------------------------------------------------------- /src/app/models/error-item.ts: -------------------------------------------------------------------------------- 1 | import { ErrorItemSource } from "./error-item-source"; 2 | 3 | export class ErrorItem { 4 | public id?: string; 5 | public source: ErrorItemSource; 6 | public code: string; 7 | public message: string; 8 | public exception?: string; 9 | public userAgent?: string; 10 | public clientVersion?: string; 11 | public serverVersion?: string; 12 | public createdAt?: string; 13 | 14 | constructor(code: string, message: string, exception: string, clientVersion: string) { 15 | this.code = code; 16 | this.message = message; 17 | this.exception = exception; 18 | this.source = ErrorItemSource.Client; 19 | this.clientVersion = clientVersion 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/models/exif.ts: -------------------------------------------------------------------------------- 1 | export class Exif { 2 | public make?: string 3 | public model?: string; 4 | public lens?: string; 5 | public createDate?: string; 6 | public focalLenIn35mmFilm?: string; 7 | public fNumber?: string; 8 | public exposureTime?: string; 9 | public photographicSensitivity?: string; 10 | public software?: string; 11 | public film?: string; 12 | public chemistry?: string; 13 | public scanner?: string; 14 | public latitude?: string; 15 | public longitude?: string; 16 | public flash?: string; 17 | public focalLength?: string; 18 | } -------------------------------------------------------------------------------- /src/app/models/file-info.ts: -------------------------------------------------------------------------------- 1 | export class FileInfo { 2 | public url = ''; 3 | public width = 0; 4 | public height = 0; 5 | public aspect = 0; 6 | } -------------------------------------------------------------------------------- /src/app/models/flexi-field.ts: -------------------------------------------------------------------------------- 1 | export class FlexiField { 2 | public id?: string; 3 | public key?: string; 4 | public value?: string; 5 | public valueHtml?: string; 6 | public isVerified?: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/models/following-import-item-status.ts: -------------------------------------------------------------------------------- 1 | export enum FollowingImportItemStatus { 2 | NotProcessed = 'notProcessed', 3 | Followed = 'followed', 4 | Sent = 'sent', 5 | Error = 'error' 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/following-import-item.ts: -------------------------------------------------------------------------------- 1 | import { FollowingImportItemStatus } from "./following-import-item-status"; 2 | 3 | export class FollowingImportItem { 4 | public id?: string; 5 | public account?: string; 6 | public showBoosts?: boolean; 7 | public languages?: string; 8 | public status?: FollowingImportItemStatus; 9 | public errorMessage?: string; 10 | public startedAt?: Date; 11 | public endedAt?: Date; 12 | public createdAt?: Date; 13 | public updatedAt?: Date; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/models/following-import-status.ts: -------------------------------------------------------------------------------- 1 | export enum FollowingImportStatus { 2 | New = 'new', 3 | Processing = 'processing', 4 | Finished = 'finished' 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/following-import.ts: -------------------------------------------------------------------------------- 1 | import { FollowingImportItem } from "./following-import-item"; 2 | import { FollowingImportStatus } from "./following-import-status"; 3 | 4 | export class FollowingImport { 5 | public id?: string; 6 | public status?: FollowingImportStatus; 7 | public startedAt?: Date; 8 | public endedAt?: Date; 9 | public createdAt?: Date; 10 | public updatedAt?: Date; 11 | public followingImportItems: FollowingImportItem[] = []; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/models/forgot-password-mode.ts: -------------------------------------------------------------------------------- 1 | export enum ForgotPasswordMode { 2 | ForgotPassword, 3 | Submitting, 4 | Success, 5 | UserNotExists 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/forgot-password.ts: -------------------------------------------------------------------------------- 1 | export class ForgotPassword { 2 | public email?: string; 3 | public redirectBaseUrl?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/models/gallery-column.ts: -------------------------------------------------------------------------------- 1 | import { GalleryStatus } from "./gallery-status"; 2 | 3 | export class GalleryColumn { 4 | public columnId: number; 5 | public size = 0; 6 | public statuses: GalleryStatus[] = []; 7 | 8 | constructor(columnId: number) { 9 | this.columnId = columnId; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/models/gallery-status.ts: -------------------------------------------------------------------------------- 1 | import { Status } from "./status"; 2 | 3 | export class GalleryStatus { 4 | public status: Status; 5 | public priority = false; 6 | 7 | constructor(status: Status, priority: boolean) { 8 | this.status = status; 9 | this.priority = priority; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/models/hashtag.ts: -------------------------------------------------------------------------------- 1 | export class Hashtag { 2 | public url: string; 3 | public name: string; 4 | 5 | constructor(url: string, name: string) { 6 | this.url = url; 7 | this.name = name; 8 | } 9 | } -------------------------------------------------------------------------------- /src/app/models/health.ts: -------------------------------------------------------------------------------- 1 | export class Health { 2 | public isDatabaseHealthy = false; 3 | public isQueueHealthy = false; 4 | public isWebPushHealthy = false; 5 | public isStorageHealthy = false; 6 | } -------------------------------------------------------------------------------- /src/app/models/http-response.ts: -------------------------------------------------------------------------------- 1 | export interface IHttpResponse { 2 | body: string; 3 | statusCode: number; 4 | statusText: string; 5 | requestUri: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/identity-token.ts: -------------------------------------------------------------------------------- 1 | export class IdentityToken { 2 | public authenticateToken: string; 3 | 4 | constructor(authenticateToken: string) { 5 | this.authenticateToken = authenticateToken; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/models/instance-blocked-domain.ts: -------------------------------------------------------------------------------- 1 | 2 | export class InstanceBlockedDomain { 3 | public id = ''; 4 | public domain = ''; 5 | public reason?: string; 6 | public createdAt?: string; 7 | public updatedAt?: string; 8 | } -------------------------------------------------------------------------------- /src/app/models/instance-statistics.ts: -------------------------------------------------------------------------------- 1 | export class InstanceStatistics { 2 | public userCount = 0; 3 | public statusCount = 0; 4 | public domainCount = 0; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/instance.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from './configuration'; 2 | import { InstanceStatistics } from './instance-statistics'; 3 | import { User } from './user'; 4 | import { Rule } from './rule'; 5 | 6 | export class Instance { 7 | public uri?: string; 8 | public title?: string; 9 | public description?: string; 10 | public longDescription?: string; 11 | public email?: string; 12 | public version?: string; 13 | public thumbnail?: string; 14 | public languages?: string[]; 15 | public rules?: Rule[]; 16 | 17 | public registrationOpened = false; 18 | public registrationByApprovalOpened = false; 19 | public registrationByInvitationsOpened = false; 20 | 21 | public configuration?: Configuration; 22 | public stats?: InstanceStatistics; 23 | public contact?: User; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/models/invitation.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./user"; 2 | 3 | export class Invitation { 4 | public id = ''; 5 | public code = ''; 6 | public user?: User; 7 | public invited?: User; 8 | public createdAt?: Date 9 | public updatedAt?: Date 10 | } 11 | -------------------------------------------------------------------------------- /src/app/models/license.ts: -------------------------------------------------------------------------------- 1 | export class License { 2 | public id?: string; 3 | public name?: string; 4 | public code?: string; 5 | public description?: string; 6 | public url?: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/models/linkable-result.ts: -------------------------------------------------------------------------------- 1 | import { ContextTimeline } from "./context-timeline"; 2 | 3 | export class LinkableResult { 4 | public maxId?: string; 5 | public minId?: string; 6 | public data: T[] = []; 7 | 8 | public context = ContextTimeline.unknown; 9 | public hashtag?: string; 10 | public category?: string; 11 | public user?: string; 12 | 13 | public static copy(value: LinkableResult): LinkableResult { 14 | const newValue = new LinkableResult(); 15 | newValue.maxId = value.maxId; 16 | newValue.minId = value.minId; 17 | newValue.data = [...value.data]; 18 | 19 | newValue.context = value.context; 20 | newValue.hashtag = value.hashtag; 21 | newValue.category = value.category; 22 | newValue.user = value.user; 23 | 24 | return newValue; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/models/location.ts: -------------------------------------------------------------------------------- 1 | import { Country } from "./country"; 2 | 3 | export class Location { 4 | public id?: string; 5 | public name?: string; 6 | public longitude?: string; 7 | public latitude?: string; 8 | public country?: Country; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/models/login-mode.ts: -------------------------------------------------------------------------------- 1 | export enum LoginMode { 2 | UserNameAndPassword, 3 | TwoFactorToken 4 | } 5 | -------------------------------------------------------------------------------- /src/app/models/login.ts: -------------------------------------------------------------------------------- 1 | export class Login { 2 | public userNameOrEmail: string; 3 | public password: string; 4 | public useCookies = true; 5 | public trustMachine = false; 6 | 7 | constructor(userNameOrEmail: string, password: string, trustMachine: boolean) { 8 | this.userNameOrEmail = userNameOrEmail; 9 | this.password = password; 10 | this.trustMachine = trustMachine; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/models/metadata.ts: -------------------------------------------------------------------------------- 1 | import { Exif } from 'src/app/models/exif'; 2 | 3 | export class Metadata { 4 | public exif?: Exif; 5 | } -------------------------------------------------------------------------------- /src/app/models/notification-type.ts: -------------------------------------------------------------------------------- 1 | export enum NotificationType { 2 | /// Someone mentioned you in their status. 3 | Mention = 'mention', 4 | 5 | /// Someone you enabled notifications for has posted a status. 6 | Status = 'status', 7 | 8 | /// Someone boosted one of your statuses. 9 | Reblog = 'reblog', 10 | 11 | /// Someone followed you. 12 | Follow = 'follow', 13 | 14 | /// Someone requested to follow you. 15 | FollowRequest = 'followRequest', 16 | 17 | /// Someone favourited one of your statuses. 18 | Favourite = 'favourite', 19 | 20 | /// A status you boosted with has been edited. 21 | Update = 'update', 22 | 23 | /// Someone signed up (optionally sent to admins). 24 | AdminSignUp = 'adminSignUp', 25 | 26 | /// A new report has been filed. 27 | AdminReport = 'adminReport', 28 | 29 | /// A new comment to status has been added. 30 | NewComment = 'newComment' 31 | } -------------------------------------------------------------------------------- /src/app/models/notification.ts: -------------------------------------------------------------------------------- 1 | import { NotificationType } from './notification-type'; 2 | import { Status } from './status'; 3 | import { User } from './user'; 4 | 5 | export class Notification { 6 | public id = ''; 7 | public notificationType?: NotificationType; 8 | public byUser?: User; 9 | public status?: Status; 10 | public mainStatus?: Status; 11 | public createdAt = ''; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/models/notifications-count.ts: -------------------------------------------------------------------------------- 1 | export class NotificationsCountDto { 2 | public amount = 0; 3 | public notificationId?: string; 4 | } -------------------------------------------------------------------------------- /src/app/models/paged-result.ts: -------------------------------------------------------------------------------- 1 | export class PagedResult { 2 | public page = 0; 3 | public size = 0; 4 | public total = 0; 5 | public data: T[] = []; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/profile-page-tab.ts: -------------------------------------------------------------------------------- 1 | export enum ProfilePageTab { 2 | Statuses, 3 | Following, 4 | Followers 5 | } -------------------------------------------------------------------------------- /src/app/models/public-settings.ts: -------------------------------------------------------------------------------- 1 | export class PublicSettings { 2 | public maximumNumberOfInvitations = 0; 3 | public isOpenAIEnabled = false; 4 | public webPushVapidPublicKey?: string; 5 | public imagesUrl?: string; 6 | public showNews = false; 7 | public showNewsForAnonymous = false; 8 | public showSharedBusinessCards = false; 9 | 10 | public patreonUrl?: string; 11 | public mastodonUrl?: string; 12 | public totalCost = 0; 13 | public usersSupport = 0; 14 | 15 | public showLocalTimelineForAnonymous = false; 16 | public showTrendingForAnonymous = false; 17 | public showEditorsChoiceForAnonymous = false; 18 | public showEditorsUsersChoiceForAnonymous = false; 19 | public showHashtagsForAnonymous = false; 20 | public showCategoriesForAnonymous = false; 21 | 22 | // Privacy and Terms of Service. 23 | public privacyPolicyUpdatedAt = '' 24 | public privacyPolicyContent = '' 25 | public termsOfServiceUpdatedAt = '' 26 | public termsOfServiceContent = '' 27 | 28 | // Custom script and style content. 29 | public customInlineScript?: string; 30 | public customInlineStyle?: string; 31 | public customFileScript?: string; 32 | public customFileStyle?: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/app/models/push-subscription.ts: -------------------------------------------------------------------------------- 1 | export class PushSubscription { 2 | public id = ''; 3 | public endpoint = ''; 4 | public userAgentPublicKey = ''; 5 | public auth = ''; 6 | public webPushNotificationsEnabled = true; 7 | public webPushMentionEnabled = true; 8 | public webPushStatusEnabled = true; 9 | public webPushReblogEnabled = true; 10 | public webPushFollowEnabled = true; 11 | public webPushFollowRequestEnabled = true; 12 | public webPushFavouriteEnabled = true; 13 | public webPushUpdateEnabled = true; 14 | public webPushAdminSignUpEnabled = true; 15 | public webPushAdminReportEnabled = true; 16 | public webPushNewCommentEnabled = true; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/models/reblog-request.ts: -------------------------------------------------------------------------------- 1 | import { StatusVisibility } from "./status-visibility"; 2 | 3 | export class ReblogRequest { 4 | public visibility: StatusVisibility 5 | 6 | constructor(visibility: StatusVisibility) { 7 | this.visibility = visibility; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/models/refresh-token.ts: -------------------------------------------------------------------------------- 1 | export class RefreshToken { 2 | public refreshToken: string; 3 | public regenerateRefreshToken = true; 4 | public useCookies = true; 5 | 6 | constructor(refreshToken: string, regenerateRefreshToken: boolean) { 7 | this.refreshToken = refreshToken; 8 | this.regenerateRefreshToken = regenerateRefreshToken; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/models/register-mode.ts: -------------------------------------------------------------------------------- 1 | export enum RegisterMode { 2 | Register, 3 | Submitting, 4 | Error 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/register-user.ts: -------------------------------------------------------------------------------- 1 | export class RegisterUser { 2 | public userName?: string; 3 | public email?: string; 4 | public password?: string; 5 | public name?: string; 6 | public securityToken?: string; 7 | public inviteToken?: string; 8 | public redirectBaseUrl?: string; 9 | public agreement?: boolean; 10 | public locale?: string; 11 | public reason?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/models/relationship.ts: -------------------------------------------------------------------------------- 1 | export class Relationship { 2 | public userId?: string 3 | 4 | /// If signed in user is following particular user (`source -> target`). 5 | public following = false; 6 | 7 | /// If signed in user is followed by particular user (`source <- target`) 8 | public followedBy = false; 9 | 10 | /// If signed in user requested follow (`source -> (request) -> target`). 11 | public requested = false; 12 | 13 | /// If signed in user has been requested by particular user (`source <- (request) <- target`). 14 | public requestedBy = false; 15 | 16 | /// If signed in user muted user's statuses. 17 | public mutedStatuses = false; 18 | 19 | /// If signed in user muted user's reblogs. 20 | public mutedReblogs = false; 21 | 22 | /// If signed in user muted user's notifications. 23 | public mutedNotifications = false; 24 | } 25 | -------------------------------------------------------------------------------- /src/app/models/report-request.ts: -------------------------------------------------------------------------------- 1 | export class ReportRequest { 2 | public reportedUserId?: string; 3 | public statusId?: string; 4 | public comment?: string; 5 | public forward = false; 6 | public category?: string; 7 | public ruleIds?: number[]; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/models/report.ts: -------------------------------------------------------------------------------- 1 | import { Status } from "./status"; 2 | import { User } from "./user"; 3 | 4 | export class Report { 5 | public id = ''; 6 | public user?: User; 7 | public reportedUser?: User; 8 | public status?: Status; 9 | public mainStatusId?: string; 10 | public comment?: string; 11 | public forward = false; 12 | public category?: string; 13 | public ruleIds?: number[]; 14 | public considerationDate?: Date; 15 | public considerationUser?: User; 16 | } 17 | -------------------------------------------------------------------------------- /src/app/models/resend-email-confirmation.ts: -------------------------------------------------------------------------------- 1 | export class ResendEmailConfirmation { 2 | public redirectBaseUrl?: string; 3 | } -------------------------------------------------------------------------------- /src/app/models/reset-password-mode.ts: -------------------------------------------------------------------------------- 1 | export enum ResetPasswordMode { 2 | ResetPassword, 3 | MissingToken, 4 | Submitting, 5 | Success 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/reset-password.ts: -------------------------------------------------------------------------------- 1 | export class ResetPassword { 2 | public forgotPasswordGuid: string; 3 | public password: string; 4 | 5 | constructor(forgotPasswordGuid = '', password = '') { 6 | this.forgotPasswordGuid = forgotPasswordGuid; 7 | this.password = password; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/models/role.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | Administrator = 'administrator', 3 | Moderator = 'moderator', 4 | Member = 'member' 5 | } -------------------------------------------------------------------------------- /src/app/models/rule.ts: -------------------------------------------------------------------------------- 1 | export class Rule { 2 | public id = ''; 3 | public order = 0; 4 | public text = ''; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/models/search-results.ts: -------------------------------------------------------------------------------- 1 | import { Hashtag } from './hashtag'; 2 | import { Status } from './status'; 3 | import { User } from './user'; 4 | 5 | export class SearchResults { 6 | public users?: User[]; 7 | public hashtags?: Hashtag[]; 8 | public statuses?: Status[]; 9 | } -------------------------------------------------------------------------------- /src/app/models/shared-business-card-message.ts: -------------------------------------------------------------------------------- 1 | export class SharedBusinessCardMessage { 2 | public id?: string; 3 | public message = ''; 4 | public addedByUser = false 5 | 6 | constructor(message: string, addedByUser: boolean) { 7 | this.message = message; 8 | this.addedByUser = addedByUser; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/models/shared-business-card-update-request.ts: -------------------------------------------------------------------------------- 1 | export class SharedBusinessCardUpdateRequest { 2 | /// Third party name. 3 | public thirdPartyName?: string; 4 | 5 | /// Third party email. 6 | public thirdPartyEmail?: string; 7 | 8 | /// Base url to web application. It's used to send email with shared card url. 9 | public sharedCardUrl = ''; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/models/shared-business-card.ts: -------------------------------------------------------------------------------- 1 | import { BusinessCard } from "./business-card"; 2 | import { SharedBusinessCardMessage } from "./shared-business-card-message"; 3 | 4 | export class SharedBusinessCard { 5 | public id?: string; 6 | public businessCardId?: string; 7 | public businessCard?: BusinessCard; 8 | public code?: string; 9 | public title = ''; 10 | public note?: string; 11 | public thirdPartyName?: string; 12 | public thirdPartyEmail?: string; 13 | public revokedAt?: Date; 14 | public createdAt?: Date; 15 | public updatedAt?: Date; 16 | 17 | public messages?: SharedBusinessCardMessage[]; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/models/status-comment.ts: -------------------------------------------------------------------------------- 1 | import { Status } from "./status"; 2 | 3 | export class StatusComment { 4 | public status: Status; 5 | public showDivider = false; 6 | 7 | constructor(status: Status, showDivider: boolean) { 8 | this.status = status; 9 | this.showDivider = showDivider; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/models/status-context.ts: -------------------------------------------------------------------------------- 1 | import { Status } from "./status" 2 | 3 | export class StatusContext { 4 | public ancestors: Status[] = [] 5 | public descendants: Status[] = [] 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/status-request.ts: -------------------------------------------------------------------------------- 1 | import { StatusVisibility } from 'src/app/models/status-visibility'; 2 | 3 | export class StatusRequest { 4 | public id = ''; 5 | public note = ''; 6 | public visibility = StatusVisibility.Public; 7 | public sensitive = false; 8 | public contentWarning?: string; 9 | public commentsDisabled = false; 10 | public replyToStatusId?: string; 11 | public attachmentIds: string[] = []; 12 | public categoryId?: string; 13 | } -------------------------------------------------------------------------------- /src/app/models/status-visibility.ts: -------------------------------------------------------------------------------- 1 | export enum StatusVisibility { 2 | Public = 'public', 3 | Followers = 'followers', 4 | Mentioned = 'mentioned' 5 | } -------------------------------------------------------------------------------- /src/app/models/status.ts: -------------------------------------------------------------------------------- 1 | import { Attachment } from 'src/app/models/attachment'; 2 | import { StatusVisibility } from 'src/app/models/status-visibility'; 3 | import { User } from './user'; 4 | import { Category } from './category'; 5 | import { Hashtag } from './hashtag'; 6 | 7 | export class Status { 8 | public id = ''; 9 | public note = ''; 10 | public noteHtml = ''; 11 | public application?: string; 12 | public createdAt = ''; 13 | public updatedAt = ''; 14 | public visibility = StatusVisibility.Public; 15 | public sensitive = false; 16 | public contentWarning?: string; 17 | public commentsDisabled = false; 18 | public replyToStatusId?: string; 19 | public isLocal = true; 20 | public activityPubId = ''; 21 | public activityPubUrl = ''; 22 | public publishedAt?: string; 23 | 24 | public repliesCount = 0; 25 | public reblogsCount = 0; 26 | public favouritesCount = 0; 27 | public favourited = false; 28 | public reblogged = false; 29 | public bookmarked = false; 30 | public featured = false; 31 | 32 | public reblog?: Status; 33 | public attachments?: Attachment[]; 34 | public user?: User; 35 | public category?: Category; 36 | public tags?: [Hashtag] 37 | } -------------------------------------------------------------------------------- /src/app/models/temporary-attachment.ts: -------------------------------------------------------------------------------- 1 | export class TemporaryAttachment { 2 | public id = ''; 3 | public url = ''; 4 | public previewUrl = ''; 5 | public description?: string; 6 | public blurhash?: string; 7 | public make?: string; 8 | public model?: string; 9 | public lens?: string; 10 | public createDate?: string; 11 | public focalLength?: string; 12 | public focalLenIn35mmFilm?: string; 13 | public fNumber?: string; 14 | public exposureTime?: string; 15 | public photographicSensitivity?: string; 16 | public software?: string; 17 | public film?: string; 18 | public chemistry?: string; 19 | public scanner?: string; 20 | public locationId?: string; 21 | public licenseId?: string; 22 | public latitude?: string; 23 | public longitude?: string; 24 | public flash?: string; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/models/trending-period.ts: -------------------------------------------------------------------------------- 1 | export enum TrendingPeriod { 2 | Daily = 'daily', 3 | Monthly = 'monthly', 4 | Yearly = 'yearly' 5 | } -------------------------------------------------------------------------------- /src/app/models/two-factor-token.ts: -------------------------------------------------------------------------------- 1 | export class TwoFactorToken { 2 | public key = ''; 3 | public label = ''; 4 | public issuer = ''; 5 | public url = ''; 6 | public backupCodes: string[] = []; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/models/user-alias.ts: -------------------------------------------------------------------------------- 1 | export class UserAlias { 2 | public id = ''; 3 | public alias = ''; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/models/user-mute-request.ts: -------------------------------------------------------------------------------- 1 | export class UserMuteRequest { 2 | public muteStatuses: boolean; 3 | public muteReblogs: boolean; 4 | public muteNotifications: boolean; 5 | public muteEnd?: Date 6 | 7 | constructor(muteStatuses: boolean, muteReblogs: boolean, muteNotifications: boolean, muteEnd?: Date) { 8 | this.muteStatuses = muteStatuses 9 | this.muteReblogs = muteReblogs 10 | this.muteNotifications = muteNotifications 11 | this.muteEnd = muteEnd 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/models/user-payload-token.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./user"; 2 | 3 | export class UserPayloadToken { 4 | public expirationDate: string; 5 | public xsrfToken: string; 6 | public userPayload: User; 7 | 8 | constructor(expirationDate: string, xsrfToken: string, userPayload: User) { 9 | this.expirationDate = expirationDate; 10 | this.xsrfToken = xsrfToken; 11 | this.userPayload = userPayload; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/models/user-setting.ts: -------------------------------------------------------------------------------- 1 | export class UserSetting { 2 | public key = ''; 3 | public value = ''; 4 | } 5 | 6 | export enum UserSettingKey { 7 | statusTextTemplate = 'status-text-template', 8 | } 9 | -------------------------------------------------------------------------------- /src/app/models/user-type.ts: -------------------------------------------------------------------------------- 1 | export enum UserType { 2 | /// Not recognized actor type. 3 | Unknown = 'unknown', 4 | 5 | /// Represents an individual person. 6 | Person = 'person', 7 | 8 | /// Describes a software application. 9 | Application = 'application', 10 | 11 | /// Represents a formal or informal collective of Actors. 12 | Group = 'group', 13 | 14 | /// Represents an organization. 15 | Organization = 'organization', 16 | 17 | /// Represents a service of any kind. 18 | Service = 'service' 19 | } 20 | -------------------------------------------------------------------------------- /src/app/models/user.ts: -------------------------------------------------------------------------------- 1 | import { FlexiField } from "./flexi-field"; 2 | import { Role } from "./role"; 3 | import { UserType } from "./user-type"; 4 | 5 | export class User { 6 | public id?: string; 7 | public type?: UserType; 8 | public url?: string; 9 | public isLocal?: boolean; 10 | public isBlocked?: boolean; 11 | public isApproved?: boolean; 12 | public userName?: string; 13 | public account?: string; 14 | public email?: string; 15 | public password?: string; 16 | public name?: string; 17 | public bio?: string; 18 | public securityToken?: string; 19 | public gravatarHash?: string; 20 | public redirectBaseUrl?: string; 21 | public locale?: string; 22 | public avatarUrl?: string; 23 | public headerUrl?: string; 24 | public agreement?: boolean; 25 | public emailWasConfirmed?: boolean; 26 | public fields?: FlexiField[]; 27 | public roles?: Role[]; 28 | public bioHtml?: string; 29 | public activityPubProfile?: string; 30 | public photosCount = 0; 31 | public statusesCount = 0; 32 | public followersCount = 0; 33 | public followingCount = 0; 34 | public twoFactorEnabled = false; 35 | public manuallyApprovesFollowers = false; 36 | public featured = false; 37 | public lastLoginDate = ''; 38 | public publishedAt?: string; 39 | public createdAt = ''; 40 | public updatedAt = ''; 41 | } 42 | -------------------------------------------------------------------------------- /src/app/pages/account/account.page.scss: -------------------------------------------------------------------------------- 1 | .account-actions { 2 | margin-left: 8px; 3 | margin-right: 8px; 4 | margin-bottom: 10px; 5 | } 6 | 7 | #avatarImage { 8 | border-radius: 50%; 9 | } 10 | 11 | #headerImage { 12 | width: 100%; 13 | max-width: 600px; 14 | } 15 | 16 | .checkbox-hint { 17 | margin-left: 44px; 18 | } 19 | 20 | .download-data { 21 | max-width: 200px; 22 | 23 | mat-icon { 24 | font-size: 20px; 25 | height: 16px; 26 | width: 16px; 27 | } 28 | } -------------------------------------------------------------------------------- /src/app/pages/article-edit/article-edit.page.scss: -------------------------------------------------------------------------------- 1 | .article-container { 2 | max-width: 640px; 3 | } -------------------------------------------------------------------------------- /src/app/pages/articles/articles.page.scss: -------------------------------------------------------------------------------- 1 | .createdAt { 2 | min-width: 200px; 3 | } 4 | 5 | .visibility { 6 | min-width: 200px; 7 | } -------------------------------------------------------------------------------- /src/app/pages/bookmarks/bookmarks.page.html: -------------------------------------------------------------------------------- 1 | @if (isReady()) { 2 |
3 |
4 |

Bookmarks

5 | 6 | @if (user(); as userInternal) { 7 | {{ fullName() }} 8 | } 9 |
10 | 11 | @if (statuses(); as statusesArray) { 12 | 13 | } 14 |
15 | } -------------------------------------------------------------------------------- /src/app/pages/bookmarks/bookmarks.page.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /src/app/pages/categories/categories.page.html: -------------------------------------------------------------------------------- 1 | @if (isReady()) { 2 |
3 |
4 |

5 | Categories 6 |
A list of categories on the server along with the photos assigned to them.
7 |

8 |
9 | 10 |
11 | } -------------------------------------------------------------------------------- /src/app/pages/categories/categories.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/pages/categories/categories.page.scss -------------------------------------------------------------------------------- /src/app/pages/category/category.page.html: -------------------------------------------------------------------------------- 1 | @if (isReady()) { 2 |
3 |
4 |

loyalty {{ category() }}

5 |
6 | 7 | @if (statuses(); as statusesArray) { 8 | 9 | } 10 |
11 | } 12 | -------------------------------------------------------------------------------- /src/app/pages/category/category.page.scss: -------------------------------------------------------------------------------- 1 | mat-icon { 2 | top: 4px; 3 | position: relative; 4 | transform: scale(1.4); 5 | opacity: 0.8; 6 | } -------------------------------------------------------------------------------- /src/app/pages/confirm-email/confirm-email.page.scss: -------------------------------------------------------------------------------- 1 | .confirm-container { 2 | width: 420px; 3 | } -------------------------------------------------------------------------------- /src/app/pages/edit-business-card/edit-business-card.page.scss: -------------------------------------------------------------------------------- 1 | .input-color-container { 2 | width: 36px; 3 | height: 36px; 4 | border-radius: 8px; 5 | background-color: white; 6 | border: 1px solid #D4D9E2; 7 | 8 | .input-color { 9 | appearance: none; 10 | background: transparent; 11 | border: none; 12 | height: 36px; 13 | width: 36px; 14 | cursor: pointer; 15 | } 16 | 17 | .input-color::-webkit-color-swatch { 18 | border-radius: 6px; 19 | border: none; 20 | } 21 | } 22 | 23 | ::ng-deep .business-card-container { 24 | width: 380px; 25 | min-height: 800px; 26 | border: 1px solid #e0e0e0; 27 | border-radius: 16px; 28 | margin-left: auto; 29 | margin-right: auto; 30 | padding: 2px; 31 | 32 | .top { 33 | border-radius: 14px 14px 0 0; 34 | } 35 | } -------------------------------------------------------------------------------- /src/app/pages/editors/editors.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/pages/editors/editors.page.scss -------------------------------------------------------------------------------- /src/app/pages/error-items/error-items.page.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } 4 | 5 | .icons { 6 | min-width: 80px; 7 | } 8 | 9 | .code { 10 | min-width: 100px; 11 | } 12 | 13 | .clientVersion { 14 | min-width: 110px; 15 | } 16 | 17 | .serverVersion { 18 | min-width: 110px; 19 | } 20 | 21 | .createdAt { 22 | min-width: 140px; 23 | } 24 | 25 | .more-button { 26 | padding: 0; 27 | 28 | .material-symbols-outlined { 29 | margin-top: 4px; 30 | } 31 | } 32 | 33 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 34 | height: 0; 35 | } -------------------------------------------------------------------------------- /src/app/pages/errors/access-forbidden/access-forbidden.page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

pan_tool Access forbidden

4 |

5 | We're sorry, you don't have access to the page. 6 | Please go back to the homepage or contact with system administrator. 7 |

8 | 9 |
10 |
-------------------------------------------------------------------------------- /src/app/pages/errors/access-forbidden/access-forbidden.page.scss: -------------------------------------------------------------------------------- 1 | .error-container { 2 | width: 560px; 3 | } -------------------------------------------------------------------------------- /src/app/pages/errors/access-forbidden/access-forbidden.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit, signal } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { fadeInAnimation } from 'src/app/animations/fade-in.animation'; 4 | 5 | @Component({ 6 | selector: 'app-access-forbidden', 7 | templateUrl: './access-forbidden.page.html', 8 | styleUrls: ['./access-forbidden.page.scss'], 9 | animations: fadeInAnimation, 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | standalone: false 12 | }) 13 | export class AccessForbiddenPage implements OnInit, OnDestroy { 14 | protected value = signal(100); 15 | private interval: NodeJS.Timeout | undefined; 16 | 17 | private router = inject(Router); 18 | 19 | async ngOnInit(): Promise { 20 | this.interval = setInterval(async ()=> { 21 | this.value.update(progress => { 22 | progress = progress - 4; 23 | if (progress < 0) { 24 | this.router.navigate(['/']); 25 | } 26 | 27 | return progress; 28 | }); 29 | }, 200); 30 | } 31 | 32 | ngOnDestroy() { 33 | if (this.interval) { 34 | clearInterval(this.interval); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/pages/errors/connection-lost/connection-lost.page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

explore Connection lost

4 |

5 | We're sorry, connection is lost. 6 | Please go back to the homepage or contact with system administrator. 7 |

8 |
9 |
-------------------------------------------------------------------------------- /src/app/pages/errors/connection-lost/connection-lost.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/pages/errors/connection-lost/connection-lost.page.scss -------------------------------------------------------------------------------- /src/app/pages/errors/connection-lost/connection-lost.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { fadeInAnimation } from 'src/app/animations/fade-in.animation'; 3 | 4 | @Component({ 5 | selector: 'app-connection-lost', 6 | templateUrl: './connection-lost.page.html', 7 | styleUrls: ['./connection-lost.page.scss'], 8 | animations: fadeInAnimation, 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | standalone: false 11 | }) 12 | export class ConnectionLostPage { 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/errors/page-not-found/page-not-found.page.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

https Page not found

4 |

5 | We're sorry, the page you requested could not be found. 6 | Please go back to the homepage or contact with system administrator. 7 |

8 | 9 |
10 |
-------------------------------------------------------------------------------- /src/app/pages/errors/page-not-found/page-not-found.page.scss: -------------------------------------------------------------------------------- 1 | .error-container { 2 | width: 560px; 3 | } -------------------------------------------------------------------------------- /src/app/pages/errors/page-not-found/page-not-found.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit, signal } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { fadeInAnimation } from 'src/app/animations/fade-in.animation'; 4 | 5 | @Component({ 6 | selector: 'app-page-not-found', 7 | templateUrl: './page-not-found.page.html', 8 | styleUrls: ['./page-not-found.page.scss'], 9 | animations: fadeInAnimation, 10 | changeDetection: ChangeDetectionStrategy.OnPush, 11 | standalone: false 12 | }) 13 | export class PageNotFoundPage implements OnInit, OnDestroy { 14 | protected value = signal(100); 15 | private interval: NodeJS.Timeout | undefined; 16 | 17 | private router = inject(Router); 18 | 19 | async ngOnInit(): Promise { 20 | this.interval = setInterval(async ()=> { 21 | this.value.update(progress => { 22 | progress = progress - 4; 23 | if (progress < 0) { 24 | this.router.navigate(['/']); 25 | } 26 | 27 | return progress; 28 | }); 29 | }, 200); 30 | } 31 | 32 | ngOnDestroy() { 33 | if (this.interval) { 34 | clearInterval(this.interval); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/pages/errors/unexpected-error/unexpected-error.page.scss: -------------------------------------------------------------------------------- 1 | .error-container { 2 | width: 560px; 3 | 4 | textarea { 5 | width: 100%; 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/pages/favourites/favourites.page.html: -------------------------------------------------------------------------------- 1 | @if (isReady()) { 2 |
3 |
4 |

Favourites

5 | 6 | @if (user(); as userInternal) { 7 | {{ fullName() }} 8 | } 9 | 10 |
11 | 12 | @if (statuses(); as statusesArray) { 13 | 14 | } 15 |
16 | } 17 | -------------------------------------------------------------------------------- /src/app/pages/favourites/favourites.page.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /src/app/pages/forgot-password/forgot-password.page.scss: -------------------------------------------------------------------------------- 1 | .forgot-container { 2 | width: 420px; 3 | } 4 | 5 | .forgot-actions { 6 | margin-left: 8px; 7 | margin-right: 8px; 8 | margin-bottom: 10px; 9 | 10 | button { 11 | margin-right: 10px; 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/pages/hashtag/hashtag.page.html: -------------------------------------------------------------------------------- 1 | @if (isReady()) { 2 |
3 |
4 |

tag {{ hashtag() }}

5 |
6 | 7 | @if (statuses(); as statusesArray) { 8 | 9 | } 10 |
11 | } 12 | -------------------------------------------------------------------------------- /src/app/pages/hashtag/hashtag.page.scss: -------------------------------------------------------------------------------- 1 | mat-icon { 2 | top: 4px; 3 | position: relative; 4 | transform: scale(1.4); 5 | opacity: 0.8; 6 | } -------------------------------------------------------------------------------- /src/app/pages/home/home.page.html: -------------------------------------------------------------------------------- 1 | @if (isReady()) { 2 | @if(!user()) { 3 | 4 | } @else { 5 | 6 | } 7 | } -------------------------------------------------------------------------------- /src/app/pages/home/home.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/pages/home/home.page.scss -------------------------------------------------------------------------------- /src/app/pages/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; 2 | import { User } from 'src/app/models/user'; 3 | import { AuthorizationService } from 'src/app/services/authorization/authorization.service'; 4 | 5 | @Component({ 6 | selector: 'app-home', 7 | templateUrl: './home.page.html', 8 | styleUrls: ['./home.page.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | standalone: false 11 | }) 12 | export class HomePage implements OnInit { 13 | protected user = signal(undefined); 14 | protected isLoggedIn = signal(false); 15 | protected isReady = signal(false); 16 | 17 | private authorizationService = inject(AuthorizationService); 18 | 19 | ngOnInit(): void { 20 | this.user.set(this.authorizationService.getUser()); 21 | this.isReady.set(true); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/pages/invitations/invitations.page.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /src/app/pages/login-callback/login-callback.page.html: -------------------------------------------------------------------------------- 1 | @if (errorMessage(); as errorMessageString) { 2 |
{{ errorMessageString }}
3 | } @else { 4 |
Authenticating...
5 | } -------------------------------------------------------------------------------- /src/app/pages/login-callback/login-callback.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/app/pages/login-callback/login-callback.page.scss -------------------------------------------------------------------------------- /src/app/pages/login/login.page.scss: -------------------------------------------------------------------------------- 1 | .login-container { 2 | width: 420px; 3 | 4 | .login-content > * { 5 | width: 100%; 6 | } 7 | 8 | .login-actions { 9 | margin-left: 8px; 10 | margin-right: 8px; 11 | margin-bottom: 10px; 12 | } 13 | 14 | .external-sign-in { 15 | text-align: center; 16 | 17 | a { 18 | min-width: 200px; 19 | margin-bottom: 10px; 20 | 21 | .icon { 22 | height: 22px; 23 | display: inline-grid; 24 | } 25 | } 26 | } 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/app/pages/news-preview/news-preview.page.scss: -------------------------------------------------------------------------------- 1 | .news-container { 2 | max-width: 640px; 3 | } 4 | 5 | mat-card-footer { 6 | padding: 0 16px 16px 16px; 7 | } 8 | 9 | ::ng-deep .news-text { 10 | font-size: 16px; 11 | line-height: 24px; 12 | font-weight: 100; 13 | 14 | img { 15 | max-width: 100%; 16 | padding-top: 30px; 17 | padding-bottom: 30px; 18 | } 19 | } -------------------------------------------------------------------------------- /src/app/pages/news/news.page.scss: -------------------------------------------------------------------------------- 1 | .news-container { 2 | max-width: 640px; 3 | } 4 | 5 | mat-card-footer { 6 | padding: 0 16px 16px 16px; 7 | } 8 | 9 | ::ng-deep .news-text { 10 | font-size: 16px; 11 | line-height: 24px; 12 | font-weight: 100; 13 | 14 | img { 15 | max-width: 100%; 16 | padding-top: 30px; 17 | padding-bottom: 30px; 18 | } 19 | } -------------------------------------------------------------------------------- /src/app/pages/notifications/notifications.page.scss: -------------------------------------------------------------------------------- 1 | .notifications-container { 2 | max-width: 640px; 3 | width: 100%; 4 | 5 | h1 { 6 | margin-bottom: 0; 7 | } 8 | 9 | .notification-wrapper { 10 | display: flex; 11 | gap: 10px; 12 | padding-bottom: 10px; 13 | padding-top: 10px; 14 | 15 | .notification-display-name { 16 | display: flex; 17 | gap: 10px; 18 | flex: 1 1 auto; 19 | overflow: hidden; 20 | 21 | .user { 22 | margin-top: 6px; 23 | 24 | .icon { 25 | position: relative; 26 | top: -10px; 27 | margin-right: 4px; 28 | } 29 | } 30 | 31 | .fullname { 32 | line-height: 16px; 33 | } 34 | 35 | .username { 36 | line-height: 16px; 37 | } 38 | } 39 | } 40 | 41 | .notifications-card { 42 | .status { 43 | img { 44 | width: 64px; 45 | height: 64px; 46 | border-radius: 5%; 47 | object-fit: cover; 48 | } 49 | } 50 | 51 | mat-icon { 52 | transform: scale(0.7); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/app/pages/preferences/preferences.page.scss: -------------------------------------------------------------------------------- 1 | .preferences-container { 2 | max-width: 640px; 3 | 4 | .actions { 5 | margin-left: 8px; 6 | margin-right: 8px; 7 | margin-bottom: 10px; 8 | } 9 | 10 | .rounded { 11 | width: 32px; 12 | height: 32px; 13 | border-radius: 15%; 14 | } 15 | 16 | .circle { 17 | width: 32px; 18 | height: 32px; 19 | border-radius: 50%; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/pages/privacy/privacy.page.html: -------------------------------------------------------------------------------- 1 | @if (isReady()) { 2 |
3 | 4 |
5 |
6 | 7 |
8 |

Privacy policy

9 |
Last update: {{ privacyPolicyUpdatedAt() }}
10 |
11 | 12 |
13 |
14 |
15 |
16 | } -------------------------------------------------------------------------------- /src/app/pages/privacy/privacy.page.scss: -------------------------------------------------------------------------------- 1 | .privacy-container { 2 | max-width: 640px; 3 | 4 | h2 { 5 | margin-top: 24px; 6 | margin-bottom: 12px; 7 | } 8 | 9 | ul { 10 | margin-bottom: 0; 11 | } 12 | 13 | li { 14 | margin-top: 6px; 15 | margin-bottom: 6px; 16 | } 17 | 18 | div { 19 | margin-top: 12px; 20 | margin-bottom: 12px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/register/register.page.scss: -------------------------------------------------------------------------------- 1 | .register-container { 2 | width: 480px; 3 | 4 | mat-form-field { 5 | width: 100%; 6 | } 7 | 8 | .register-actions { 9 | margin-left: 8px; 10 | margin-right: 8px; 11 | margin-bottom: 10px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/pages/reports/reports.page.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } 4 | 5 | .submenu { 6 | .mat-icon { 7 | margin: 0; 8 | } 9 | } 10 | 11 | .action-button { 12 | min-width: 90px; 13 | } -------------------------------------------------------------------------------- /src/app/pages/reset-password/reset-password.page.scss: -------------------------------------------------------------------------------- 1 | .reset-container { 2 | width: 420px; 3 | } 4 | 5 | .reset-actions { 6 | margin-left: 8px; 7 | margin-right: 8px; 8 | margin-bottom: 10px; 9 | 10 | button { 11 | margin-right: 10px; 12 | } 13 | } -------------------------------------------------------------------------------- /src/app/pages/search/search.page.scss: -------------------------------------------------------------------------------- 1 | .search { 2 | max-width: 640px; 3 | 4 | mat-form-field { 5 | width: 100%; 6 | } 7 | 8 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 9 | height: 0; 10 | } 11 | } -------------------------------------------------------------------------------- /src/app/pages/settings/settings.page.scss: -------------------------------------------------------------------------------- 1 | .settings-container { 2 | max-width: 640px; 3 | } 4 | 5 | .settings-headers-align .mat-expansion-panel-header-description { 6 | justify-content: space-between; 7 | align-items: center; 8 | } 9 | 10 | .settings-headers-align .mat-mdc-form-field + .mat-mdc-form-field { 11 | margin-left: 8px; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/pages/shared-card-public/shared-card-public.page.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep .business-card-container { 2 | .bottom { 3 | max-width: 640px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | } 7 | } 8 | 9 | mat-form-field { 10 | width: 100%; 11 | } 12 | 13 | .username { 14 | margin-top: -5px; 15 | 16 | .fullname { 17 | line-height: 14px; 18 | 19 | a { 20 | line-height: 14px; 21 | } 22 | } 23 | 24 | .account { 25 | line-height: 14px; 26 | } 27 | } 28 | 29 | .business-card-messages { 30 | max-width: 640px; 31 | margin-left: auto; 32 | margin-right: auto; 33 | 34 | .business-card-message-text { 35 | max-width: 400px; 36 | border-radius: 24px; 37 | padding: 8px 16px; 38 | display: inline-block; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/pages/shared-card/shared-card.page.scss: -------------------------------------------------------------------------------- 1 | ::ng-deep .business-card-container { 2 | .bottom { 3 | max-width: 640px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | } 7 | } 8 | 9 | mat-form-field { 10 | width: 100%; 11 | } 12 | 13 | .title-date { 14 | min-width: 180px; 15 | } 16 | 17 | .username { 18 | margin-top: -5px; 19 | 20 | .fullname { 21 | line-height: 14px; 22 | 23 | a { 24 | line-height: 14px; 25 | } 26 | } 27 | 28 | .account { 29 | line-height: 14px; 30 | } 31 | } 32 | 33 | .business-card-messages { 34 | max-width: 640px; 35 | margin-left: auto; 36 | margin-right: auto; 37 | 38 | .business-card-message-text { 39 | max-width: 400px; 40 | border-radius: 24px; 41 | padding: 8px 16px; 42 | display: inline-block; 43 | } 44 | } -------------------------------------------------------------------------------- /src/app/pages/shared-cards/shared-cards.page.scss: -------------------------------------------------------------------------------- 1 | @media only screen and (min-width: 960px) { 2 | .page-buttons { 3 | min-width: 400px; 4 | text-align: right; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/app/pages/support/support.page.scss: -------------------------------------------------------------------------------- 1 | app-mini-user-card { 2 | display: inline-block; 3 | vertical-align: text-top; 4 | } 5 | 6 | dl { 7 | margin-left: 20px; 8 | } 9 | 10 | dd { 11 | margin: 0; 12 | font-weight: 200; 13 | } 14 | 15 | @media only screen and (min-width: 768px) { 16 | dd { 17 | display: inline; 18 | } 19 | } 20 | 21 | dt { 22 | display: inline-block; 23 | width: 240px; 24 | font-weight: 400; 25 | margin-top: 6px; 26 | } 27 | 28 | dd::after { 29 | content: ""; 30 | display: block; 31 | } 32 | 33 | ul { 34 | list-style-type: none; 35 | padding-left: 20px; 36 | } 37 | 38 | .patreon img { 39 | position: relative; 40 | top: 3px; 41 | } 42 | 43 | .description { 44 | max-width: 640px; 45 | } -------------------------------------------------------------------------------- /src/app/pages/terms/terms.page.html: -------------------------------------------------------------------------------- 1 | @if (isReady()) { 2 |
3 | 4 |
5 |
6 | 7 |
8 |

Terms of Service

9 |
Last update: {{ termsOfServiceUpdatedAt() }}
10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | } 18 | -------------------------------------------------------------------------------- /src/app/pages/terms/terms.page.scss: -------------------------------------------------------------------------------- 1 | .terms-container { 2 | max-width: 640px; 3 | 4 | h2 { 5 | margin-top: 24px; 6 | margin-bottom: 12px; 7 | } 8 | 9 | ul { 10 | margin-bottom: 0; 11 | } 12 | 13 | li { 14 | margin-top: 6px; 15 | margin-bottom: 6px; 16 | } 17 | 18 | div { 19 | margin-top: 12px; 20 | margin-bottom: 12px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/pages/trending/trending.page.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } -------------------------------------------------------------------------------- /src/app/pages/upload/upload.page.scss: -------------------------------------------------------------------------------- 1 | .cdk-drag-preview { 2 | box-sizing: border-box; 3 | border-radius: 4px; 4 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 5 | 0 8px 10px 1px rgba(0, 0, 0, 0.14), 6 | 0 3px 14px 2px rgba(0, 0, 0, 0.12); 7 | } 8 | 9 | .cdk-drag-placeholder { 10 | opacity: 0; 11 | } 12 | 13 | .cdk-drag-animating { 14 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 15 | } 16 | 17 | .edit-template { 18 | min-width: 24px; 19 | padding-left: 8px; 20 | padding-right: 8px; 21 | 22 | mat-icon { 23 | margin: 0; 24 | } 25 | } 26 | 27 | .text-template { 28 | min-width: 24px; 29 | padding-left: 8px; 30 | padding-right: 8px; 31 | 32 | mat-icon { 33 | margin: 0; 34 | } 35 | 36 | span { 37 | margin-left: 8px; 38 | } 39 | } -------------------------------------------------------------------------------- /src/app/pages/users/users.page.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | margin: 0; 3 | } 4 | 5 | .more-button { 6 | padding: 0; 7 | 8 | .material-symbols-outlined { 9 | margin-top: 4px; 10 | } 11 | } 12 | 13 | .user-name-link a { 14 | width: 140px; 15 | white-space: nowrap; 16 | overflow: hidden; 17 | text-overflow: ellipsis; 18 | display: block; 19 | } 20 | 21 | .name { 22 | width: 140px; 23 | white-space: nowrap; 24 | overflow: hidden; 25 | text-overflow: ellipsis; 26 | display: block; 27 | } 28 | 29 | 30 | ::ng-deep .mat-mdc-form-field-subscript-wrapper { 31 | height: 0; 32 | } -------------------------------------------------------------------------------- /src/app/pipes/ago.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from "@angular/core"; 2 | 3 | @Pipe({ 4 | name:'ago', 5 | standalone: false 6 | }) 7 | export class AgoPipe implements PipeTransform { 8 | private readonly intervals: Record = { 9 | 'day': 86400, 10 | 'hour': 3600, 11 | 'minute': 60, 12 | 'second': 1 13 | }; 14 | 15 | 16 | transform(value: any): any { 17 | if (value) { 18 | const currentDate = new Date(); 19 | const displayedDate = new Date(value); 20 | 21 | if (displayedDate.toString() === 'Invalid Date') { 22 | return '' 23 | } 24 | 25 | const seconds = Math.floor((currentDate.getTime() - displayedDate.getTime()) / 1000); 26 | if (seconds < 59) { 27 | return 'few seconds ago'; 28 | } 29 | 30 | let counter; 31 | for (const interval in this.intervals) { 32 | counter = Math.floor(seconds / this.intervals[interval]); 33 | if (counter > 0) { 34 | if (counter === 1) { 35 | return `${counter} ${interval} ago`; 36 | } else { 37 | return `${counter} ${interval}s ago`; 38 | } 39 | } 40 | } 41 | } 42 | 43 | return ''; 44 | } 45 | } -------------------------------------------------------------------------------- /src/app/pipes/pipes.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { SanitizeHtmlPipe } from './sanitize-html.pipe'; 3 | import { AgoPipe } from './ago.pipe'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | SanitizeHtmlPipe, 8 | AgoPipe 9 | ], 10 | imports: [ 11 | 12 | ], 13 | exports: [ 14 | SanitizeHtmlPipe, 15 | AgoPipe 16 | ] 17 | }) 18 | export class PipesModule { } 19 | -------------------------------------------------------------------------------- /src/app/pipes/sanitize-html.pipe.ts: -------------------------------------------------------------------------------- 1 | import { inject, Pipe, PipeTransform } from '@angular/core'; 2 | import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; 3 | 4 | @Pipe({ 5 | name: 'sanitizeHtml', 6 | standalone: false 7 | }) 8 | export class SanitizeHtmlPipe implements PipeTransform { 9 | private sanitizer = inject(DomSanitizer); 10 | 11 | transform(value: string): SafeHtml { 12 | return this.sanitizer.bypassSecurityTrustHtml(value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/services/authorization/authorization-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 2 | import { AuthorizationService } from './authorization.service'; 3 | import { inject } from '@angular/core'; 4 | 5 | export const authorizationGuard = async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { 6 | const authorizationService = inject(AuthorizationService); 7 | const router = inject(Router); 8 | 9 | const isLoggedIn = await authorizationService.isLoggedIn(); 10 | if (!isLoggedIn) { 11 | await authorizationService.signOut(); 12 | await router.navigate(['/login'], { queryParams: { returnUrl: state.url } }); 13 | 14 | return false; 15 | } 16 | 17 | return true; 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/services/authorization/logged-out-guard.service.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { AuthorizationService } from './authorization.service'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | export const loggedOutGuard = async (_: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { 7 | const authorizationService = inject(AuthorizationService); 8 | const router = inject(Router); 9 | 10 | const isLoggedIn = await authorizationService.isLoggedIn(); 11 | if (isLoggedIn) { 12 | router.navigate(['/home']); 13 | 14 | return false; 15 | } 16 | 17 | return true; 18 | }; 19 | -------------------------------------------------------------------------------- /src/app/services/common/file-size.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class FileSizeService { 7 | 8 | public getHumanFileSize(bytes: number, places: number) { 9 | const thresh = 1024; 10 | const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; 11 | 12 | if (Math.abs(bytes) < thresh) { 13 | return bytes + ' B'; 14 | } 15 | 16 | let u = -1; 17 | const r = 10**places; 18 | 19 | do { 20 | bytes /= thresh; 21 | ++u; 22 | } while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1); 23 | 24 | 25 | return bytes.toFixed(places) + ' ' + units[u]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/services/common/focus-tracker.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ providedIn: 'root' }) 5 | export class FocusTrackerService { 6 | public $isFocused = new BehaviorSubject(false); 7 | 8 | setFocusState(focused: boolean) { 9 | this.$isFocused.next(focused); 10 | } 11 | 12 | get isCurrentlyFocused(): boolean { 13 | return this.$isFocused.value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/services/common/loading.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class LoadingService { 8 | static singletonInstance: LoadingService; 9 | 10 | loadingStateChanges: BehaviorSubject = new BehaviorSubject(false); 11 | 12 | constructor() { 13 | if (!LoadingService.singletonInstance) { 14 | LoadingService.singletonInstance = this; 15 | } 16 | 17 | return LoadingService.singletonInstance; 18 | } 19 | 20 | showLoader(): void { 21 | this.loadingStateChanges.next(true); 22 | } 23 | 24 | hideLoader(): void { 25 | this.loadingStateChanges.next(false); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/services/common/messages.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { MatSnackBar } from '@angular/material/snack-bar'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class MessagesService { 8 | private matSnackBar = inject(MatSnackBar); 9 | 10 | showSuccess(message: string): void { 11 | this.matSnackBar.open(message, 'Dismiss', { 12 | duration: 5000, 13 | verticalPosition: 'top', 14 | panelClass: ['message-success'] 15 | }); 16 | } 17 | 18 | showError(message: string): void { 19 | this.matSnackBar.open(message, 'Dismiss', { 20 | duration: 5000, 21 | verticalPosition: 'top', 22 | panelClass: ['message-error'] 23 | }); 24 | } 25 | 26 | showServerError(error: any): void { 27 | const reason = error?.error?.reason ?? 'Unknown error.'; 28 | this.matSnackBar.open(reason, 'Dismiss', { 29 | duration: 5000, 30 | verticalPosition: 'top', 31 | panelClass: ['message-error'] 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/services/common/random-generator.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class RandomGeneratorService { 7 | readonly characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 8 | 9 | 10 | generateString(length: number) { 11 | let result = ''; 12 | const charactersLength = this.characters.length; 13 | 14 | let counter = 0; 15 | while (counter < length) { 16 | result += this.characters.charAt(Math.floor(Math.random() * charactersLength)); 17 | counter += 1; 18 | } 19 | 20 | return result; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/services/common/routing-state.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { NavigationEnd, Router } from '@angular/router'; 3 | import { filter } from 'rxjs'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class RoutingStateService { 9 | private history: string[] = []; 10 | private router = inject(Router); 11 | 12 | public startRoutingListener(): void { 13 | this.router.events 14 | .pipe(filter(event => event instanceof NavigationEnd)) 15 | .subscribe(async (event) => { 16 | const navigationEndEvent = event as NavigationEnd; 17 | if (navigationEndEvent.urlAfterRedirects) { 18 | this.history.push(navigationEndEvent.urlAfterRedirects); 19 | 20 | if (this.history.length >= 10) { 21 | this.history.shift(); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | public getHistory(): string[] { 28 | return this.history; 29 | } 30 | 31 | public getPreviousUrl(): string | undefined { 32 | return this.history[this.history.length - 2] || undefined; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/services/common/user-display.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { User } from 'src/app/models/user'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class UserDisplayService { 8 | 9 | displayName(user: User | undefined): string { 10 | if(!user) { 11 | return ''; 12 | } 13 | 14 | if (user.name && user.name.trim().length > 0) { 15 | return user.name; 16 | } 17 | 18 | if (user.userName) { 19 | return `@${user.userName}`; 20 | } 21 | 22 | return ''; 23 | } 24 | 25 | verifiedUrl(user: User | undefined): string | undefined { 26 | if(!user || !user.fields || user.fields.length === 0) { 27 | return undefined; 28 | } 29 | 30 | for(const field of user.fields) { 31 | if (field.isVerified) { 32 | return field.valueHtml; 33 | } 34 | } 35 | 36 | return undefined; 37 | } 38 | } -------------------------------------------------------------------------------- /src/app/services/http/archives.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { WindowService } from '../common/window.service'; 5 | import { Archive } from 'src/app/models/archive'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class ArchivesService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async get(): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/archives'); 16 | return await firstValueFrom(event$); 17 | } 18 | 19 | public async create(): Promise { 20 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/archives', null); 21 | return await firstValueFrom(event$); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/services/http/auth-clients.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { inject, Injectable } from '@angular/core'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { AuthClient } from 'src/app/models/auth-client'; 5 | import { WindowService } from '../common/window.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class AuthClientsService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async getList(): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/auth-clients'); 16 | return await firstValueFrom(event$); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/services/http/avatars.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { WindowService } from '../common/window.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AvatarsService { 10 | private httpClient = inject(HttpClient); 11 | private windowService = inject(WindowService); 12 | 13 | public async uploadAvatar(userName: string, formData: FormData): Promise { 14 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/avatars/@' + userName, formData); 15 | await firstValueFrom(event$); 16 | } 17 | 18 | public async deleteAvatar(userName: string): Promise { 19 | const event$ = this.httpClient.delete(this.windowService.apiUrl() + '/api/v1/avatars/@' + userName); 20 | await firstValueFrom(event$); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/services/http/bookmarks.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { Status } from 'src/app/models/status'; 5 | import { WindowService } from '../common/window.service'; 6 | import { LinkableResult } from 'src/app/models/linkable-result'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class BookmarksService { 12 | private httpClient = inject(HttpClient); 13 | private windowService = inject(WindowService); 14 | 15 | public async list(minId?: string, maxId?: string, sinceId?: string, limit?: number): Promise> { 16 | const event$ = this.httpClient.get>(this.windowService.apiUrl() + `/api/v1/bookmarks?minId=${minId ?? ''}&maxId=${maxId ?? ''}&sinceId=${sinceId ?? ''}&limit=${limit ?? ''}`); 17 | return await firstValueFrom(event$); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/services/http/countries.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { Country } from 'src/app/models/country'; 5 | import { WindowService } from '../common/window.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class CountriesService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async all(): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/countries'); 16 | return await firstValueFrom(event$); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/services/http/exports.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { WindowService } from '../common/window.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ExportsService { 10 | private httpClient = inject(HttpClient); 11 | private windowService = inject(WindowService); 12 | 13 | public async following(): Promise { 14 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/exports/following', { responseType: 'blob' }); 15 | return await firstValueFrom(event$); 16 | } 17 | 18 | public async bookmarks(): Promise { 19 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/exports/bookmarks', { responseType: 'blob' }); 20 | return await firstValueFrom(event$); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/services/http/favourites.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { Status } from 'src/app/models/status'; 5 | import { WindowService } from '../common/window.service'; 6 | import { LinkableResult } from 'src/app/models/linkable-result'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class FavouritesService { 12 | private httpClient = inject(HttpClient); 13 | private windowService = inject(WindowService); 14 | 15 | public async list(minId?: string, maxId?: string, sinceId?: string, limit?: number): Promise> { 16 | const event$ = this.httpClient.get>(this.windowService.apiUrl() + `/api/v1/favourites?minId=${minId ?? ''}&maxId=${maxId ?? ''}&sinceId=${sinceId ?? ''}&limit=${limit ?? ''}`); 17 | return await firstValueFrom(event$); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/services/http/follow-requests.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { Relationship } from 'src/app/models/relationship'; 5 | import { WindowService } from '../common/window.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class FollowRequestsService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async get(page: number, size: number): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + `/api/v1/follow-requests?page=${page ?? ''}&size=${size ?? ''}`); 16 | return await firstValueFrom(event$); 17 | } 18 | 19 | public async approve(userId: string): Promise { 20 | const event$ = this.httpClient.post(this.windowService.apiUrl() + `/api/v1/follow-requests/${userId}/approve`, null); 21 | return await firstValueFrom(event$); 22 | } 23 | 24 | public async reject(userId: string): Promise { 25 | const event$ = this.httpClient.post(this.windowService.apiUrl() + `/api/v1/follow-requests/${userId}/reject`, null); 26 | return await firstValueFrom(event$); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/services/http/following-imports.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { WindowService } from '../common/window.service'; 5 | import { PagedResult } from 'src/app/models/paged-result'; 6 | import { FollowingImport } from 'src/app/models/following-import'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class FollowingImportsService { 12 | private httpClient = inject(HttpClient); 13 | private windowService = inject(WindowService); 14 | 15 | public async get(page: number, size: number): Promise> { 16 | const event$ = this.httpClient.get>(this.windowService.apiUrl() + `/api/v1/following-imports?page=${page}&size=${size}`); 17 | return await firstValueFrom(event$); 18 | } 19 | 20 | public async upload(formData: FormData): Promise { 21 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/following-imports', formData); 22 | return await firstValueFrom(event$); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/services/http/forgot-password.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { ForgotPassword } from 'src/app/models/forgot-password'; 5 | import { ResetPassword } from 'src/app/models/reset-password'; 6 | import { WindowService } from '../common/window.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ForgotPasswordService { 12 | private httpClient = inject(HttpClient); 13 | private windowService = inject(WindowService); 14 | 15 | public async token(forgotPassword: ForgotPassword): Promise { 16 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/account/forgot/token', forgotPassword); 17 | return await firstValueFrom(event$); 18 | } 19 | 20 | public async confirm(resetPassword: ResetPassword): Promise { 21 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/account/forgot/confirm', resetPassword); 22 | return await firstValueFrom(event$); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/services/http/headers.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { WindowService } from '../common/window.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class HeadersService { 10 | private httpClient = inject(HttpClient); 11 | private windowService = inject(WindowService); 12 | 13 | public async uploadHeader(userName: string, formData: FormData): Promise { 14 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/headers/@' + userName, formData); 15 | await firstValueFrom(event$); 16 | } 17 | 18 | public async deleteHeader(userName: string): Promise { 19 | const event$ = this.httpClient.delete(this.windowService.apiUrl() + '/api/v1/headers/@' + userName); 20 | await firstValueFrom(event$); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/services/http/health.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { inject, Injectable } from '@angular/core'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { WindowService } from '../common/window.service'; 5 | import { Health } from '../../models/health'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class HealthService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async get(): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/health'); 16 | return await firstValueFrom(event$); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/services/http/identity.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { inject, Injectable } from '@angular/core'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { UserPayloadToken } from 'src/app/models/user-payload-token'; 5 | import { IdentityToken } from 'src/app/models/identity-token'; 6 | import { WindowService } from '../common/window.service'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class IdentityService { 12 | private httpClient = inject(HttpClient); 13 | private windowService = inject(WindowService); 14 | 15 | public async login(identityToken: IdentityToken): Promise { 16 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/identity/login', identityToken); 17 | return await firstValueFrom(event$); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/services/http/instance.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { inject, Injectable } from '@angular/core'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { Instance } from 'src/app/models/instance'; 5 | import { WindowService } from '../common/window.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class InstanceService { 11 | private _instance?: Instance; 12 | 13 | private httpClient = inject(HttpClient); 14 | private windowService = inject(WindowService); 15 | 16 | public get instance(): Instance | undefined { 17 | return this._instance; 18 | } 19 | 20 | public async load(): Promise { 21 | this._instance = await this.get(); 22 | } 23 | 24 | public isRegistrationEnabled(): boolean { 25 | return this.instance?.registrationOpened === true 26 | || this.instance?.registrationByApprovalOpened === true 27 | || this.instance?.registrationByInvitationsOpened === true; 28 | } 29 | 30 | private async get(): Promise { 31 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/instance'); 32 | return await firstValueFrom(event$); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/services/http/invitations.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { Invitation } from 'src/app/models/invitation'; 5 | import { WindowService } from '../common/window.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class InvitationsService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async get(): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/invitations'); 16 | return await firstValueFrom(event$); 17 | } 18 | 19 | public async generate(): Promise { 20 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/invitations/generate', null); 21 | return await firstValueFrom(event$); 22 | } 23 | 24 | public async delete(id: string): Promise { 25 | const event$ = this.httpClient.delete(this.windowService.apiUrl() + '/api/v1/invitations/' + id); 26 | await firstValueFrom(event$); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/services/http/liceses.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { WindowService } from '../common/window.service'; 5 | import { License } from 'src/app/models/license'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class LicensesService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async all(): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/licenses'); 16 | return await firstValueFrom(event$); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/services/http/locations.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { Location } from 'src/app/models/location'; 5 | import { WindowService } from '../common/window.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class LocationsService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async search(code: string, query?: string): Promise { 15 | if (!query || query === '') { 16 | return []; 17 | } 18 | 19 | const event$ = this.httpClient.get(this.windowService.apiUrl() + `/api/v1/locations?code=${code}&query=${query}`); 20 | return await firstValueFrom(event$); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/services/http/relationships.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { Relationship } from 'src/app/models/relationship'; 5 | import { WindowService } from '../common/window.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class RelationshipsService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async get(userId: string): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/relationships?id[]=' + userId); 16 | const relationships = await firstValueFrom(event$); 17 | 18 | return relationships.length === 1 ? relationships[0] : new Relationship(); 19 | } 20 | 21 | public async getAll(userIds: string[]): Promise { 22 | const queryParams = userIds.join('&id[]=') 23 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/relationships?id[]=' + queryParams); 24 | return await firstValueFrom(event$); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/services/http/search.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { SearchResults } from 'src/app/models/search-results'; 5 | import { WindowService } from '../common/window.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class SearchService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async search(query: string, type: string): Promise { 15 | const queryWithoutHashtags = query.replaceAll('#', ''); 16 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/search?query=' + queryWithoutHashtags + '&type=' + type); 17 | return await firstValueFrom(event$); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/services/http/user-aliases.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { inject, Injectable } from '@angular/core'; 3 | import { firstValueFrom } from 'rxjs'; 4 | import { WindowService } from '../common/window.service'; 5 | import { UserAlias } from 'src/app/models/user-alias'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class UserAliasesService { 11 | private httpClient = inject(HttpClient); 12 | private windowService = inject(WindowService); 13 | 14 | public async get(): Promise { 15 | const event$ = this.httpClient.get(this.windowService.apiUrl() + '/api/v1/user-aliases'); 16 | return await firstValueFrom(event$); 17 | } 18 | 19 | public async create(userAlias: UserAlias): Promise { 20 | const event$ = this.httpClient.post(this.windowService.apiUrl() + '/api/v1/user-aliases', userAlias); 21 | return await firstValueFrom(event$); 22 | } 23 | 24 | public async delete(id: string): Promise { 25 | const event$ = this.httpClient.delete(this.windowService.apiUrl() + '/api/v1/user-aliases/' + id); 26 | return await firstValueFrom(event$); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/validators/directives/autocomplete-valid.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, input } from '@angular/core'; 2 | import { NG_VALIDATORS, Validator, UntypedFormControl, ValidationErrors } from '@angular/forms'; 3 | 4 | @Directive({ 5 | selector: '[appAutocompleteValid]', 6 | providers: [{ 7 | provide: NG_VALIDATORS, 8 | useExisting: AutocompleteValidDirective, 9 | multi: true 10 | }], 11 | standalone: false 12 | }) 13 | 14 | export class AutocompleteValidDirective implements Validator { 15 | public appAutocompleteValid = input(true); 16 | 17 | validate(formControl: UntypedFormControl): ValidationErrors | null { 18 | if (!this.appAutocompleteValid()) { 19 | return null; 20 | } 21 | 22 | if (!formControl.value) { 23 | return null; 24 | } 25 | 26 | const isCorrect = this.isObject(formControl.value); 27 | return isCorrect ? null : { appAutocompleteValid: { valid: false } }; 28 | } 29 | 30 | private isObject(obj: any): boolean { 31 | return obj != null && obj.constructor.name === 'Object'; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/validators/directives/max-length-validator.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input } from '@angular/core'; 2 | import { NG_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; 3 | 4 | @Directive({ 5 | selector: '[appMaxLength]', 6 | providers: [{ 7 | provide: NG_VALIDATORS, 8 | useExisting: MaxLengthValidatorDirective, 9 | multi: true 10 | }], 11 | standalone: false 12 | }) 13 | 14 | export class MaxLengthValidatorDirective implements Validator { 15 | 16 | private lenght?: number; 17 | 18 | @Input('appMaxLength') set maxLength(value: string) { 19 | this.lenght = Number(value); 20 | } 21 | 22 | validate(formControl: FormControl): ValidationErrors | null { 23 | if (!formControl.value || !this.lenght) { 24 | return null; 25 | } 26 | 27 | const isCorrect = formControl.value.length > this.lenght ? false : true; 28 | return isCorrect ? null : { appMaxLength: { valid: false } }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/validators/models/password-errors.ts: -------------------------------------------------------------------------------- 1 | export class PasswordErrors { 2 | public length = true; 3 | public lowercase = true; 4 | public uppercase = true; 5 | public symbol = true; 6 | 7 | public isValid(): boolean { 8 | return !this.length && !this.lowercase && !this.uppercase && !this.symbol; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/validators/validations.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | 4 | import { UniqueEmailValidatorDirective } from './directives/unique-email-validator.directive'; 5 | import { UniqueUserNameValidatorDirective } from './directives/unique-user-name-validator.directive'; 6 | import { PasswordValidatorDirective } from './directives/password-validator.directive'; 7 | import { MaxLengthValidatorDirective } from './directives/max-length-validator.directive'; 8 | import { AutocompleteValidDirective } from './directives/autocomplete-valid.directive'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | UniqueEmailValidatorDirective, 13 | UniqueUserNameValidatorDirective, 14 | PasswordValidatorDirective, 15 | MaxLengthValidatorDirective, 16 | AutocompleteValidDirective 17 | ], 18 | imports: [ 19 | CommonModule 20 | ], 21 | exports: [ 22 | UniqueEmailValidatorDirective, 23 | UniqueUserNameValidatorDirective, 24 | PasswordValidatorDirective, 25 | MaxLengthValidatorDirective, 26 | AutocompleteValidDirective 27 | ] 28 | }) 29 | export class ValidationsModule { } 30 | -------------------------------------------------------------------------------- /src/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /src/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/avatar.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/avatar.afdesign -------------------------------------------------------------------------------- /src/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/avatar.png -------------------------------------------------------------------------------- /src/assets/avatar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/beta-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/beta-dark.png -------------------------------------------------------------------------------- /src/assets/beta-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/beta-light.png -------------------------------------------------------------------------------- /src/assets/beta.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/beta.afdesign -------------------------------------------------------------------------------- /src/assets/camera.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/camera.afdesign -------------------------------------------------------------------------------- /src/assets/face.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/face.afdesign -------------------------------------------------------------------------------- /src/assets/fonts/material-symbols-HRDtGgxmEY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/fonts/material-symbols-HRDtGgxmEY.woff2 -------------------------------------------------------------------------------- /src/assets/fonts/roboto-12DfhPfYyt.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/fonts/roboto-12DfhPfYyt.woff2 -------------------------------------------------------------------------------- /src/assets/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/header.jpg -------------------------------------------------------------------------------- /src/assets/icons/icon-1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-1024x1024.png -------------------------------------------------------------------------------- /src/assets/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-152x152.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/assets/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-96x96.png -------------------------------------------------------------------------------- /src/assets/icons/icon-dark.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-dark.afdesign -------------------------------------------------------------------------------- /src/assets/icons/icon-light.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/icons/icon-light.afdesign -------------------------------------------------------------------------------- /src/assets/logo-dark.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/logo-dark.afdesign -------------------------------------------------------------------------------- /src/assets/logo-light.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/logo-light.afdesign -------------------------------------------------------------------------------- /src/assets/news.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/news.afdesign -------------------------------------------------------------------------------- /src/assets/patreon-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/patreon-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/user.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/user.afdesign -------------------------------------------------------------------------------- /src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/assets/user.png -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false, 7 | version: '1.15.0-buildx', 8 | recaptchaKey: '' 9 | }; 10 | 11 | /* 12 | * For easier debugging in development mode, you can import the following file 13 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 14 | * 15 | * This import should be commented out in production mode because it will have a negative impact 16 | * on performance if an error is thrown. 17 | */ 18 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 19 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VernissageApp/VernissageWeb/fecea7fd7f108a7ce7eba632108c1133970efe69/src/favicon.ico -------------------------------------------------------------------------------- /src/main.server.ts: -------------------------------------------------------------------------------- 1 | export { AppServerModule as default } from './app/app.module.server'; 2 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { AppModule } from './app/app.module'; 3 | 4 | platformBrowserDynamic() 5 | .bootstrapModule(AppModule) 6 | .catch(err => console.error(err)); 7 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | import 'hammerjs'; -------------------------------------------------------------------------------- /src/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | 3 | User-agent: GPTBot 4 | Disallow: / 5 | 6 | User-agent: * 7 | Disallow: /assets/ 8 | Disallow: /styles/ -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | // Import angular service worker. 2 | importScripts("./ngsw-worker.js"); 3 | 4 | // Create custom service worker logic. 5 | (() => { 6 | var VernissageServiceWorker = class { 7 | constructor( scope2 ) { 8 | this.scope = scope2; 9 | this.scope.addEventListener("push", (event) => this.myOnPush(event)); 10 | } 11 | 12 | myOnPush(event) { 13 | if (!event.data) { 14 | return; 15 | } 16 | event.waitUntil( this.setBadgeCount(event.data.json()) ); 17 | } 18 | 19 | async setBadgeCount( data ) { 20 | const badgeCount = data.notification?.data?.badgeCount; 21 | if (navigator.setAppBadge) { 22 | if (badgeCount && badgeCount > 0) { 23 | await navigator.setAppBadge(badgeCount); 24 | } else { 25 | await navigator.clearAppBadge(); 26 | } 27 | } else { 28 | console.error("Badging not supported on this platform."); 29 | } 30 | } 31 | } 32 | 33 | var scope = self; 34 | new VernissageServiceWorker( scope ); 35 | })(); -------------------------------------------------------------------------------- /src/styles/messages-theme.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @mixin messages-theme($theme) { 3 | $primary: mat.m2-get-color-from-palette(map-get($theme, primary)); 4 | $primary-text: mat.m2-get-color-from-palette(map-get($theme, primary), default-contrast); 5 | $warn: mat.m2-get-color-from-palette(map-get($theme, warn)); 6 | $warn-text: mat.m2-get-color-from-palette(map-get($theme, warn), default-contrast); 7 | 8 | .message-error { 9 | color: $warn-text; 10 | background-color: $warn; 11 | } 12 | 13 | .message-error .mat-simple-snackbar-action { 14 | color: $warn-text; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/styles/notification-icons.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @mixin notification-icons($palette) { 3 | .notifications-container { 4 | .notifications-card { 5 | .mention { 6 | color: map-get($palette, 50); 7 | } 8 | 9 | .status { 10 | color: map-get($palette, 100); 11 | } 12 | 13 | .reblog { 14 | color: map-get($palette, 200); 15 | } 16 | 17 | .follow { 18 | color: map-get($palette, 300); 19 | } 20 | 21 | .follow-request { 22 | color: map-get($palette, 400); 23 | } 24 | 25 | .favourite { 26 | color: map-get($palette, 500); 27 | } 28 | 29 | .update { 30 | color: map-get($palette, 600); 31 | } 32 | 33 | .admin-sign-up { 34 | color: map-get($palette, 700); 35 | } 36 | 37 | .admin-report{ 38 | color: map-get($palette, 800); 39 | } 40 | 41 | .new-comments { 42 | color: map-get($palette, 900); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [ 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/main.ts", 12 | "src/polyfills.ts", 13 | "src/main.server.ts", 14 | "server.ts" 15 | ], 16 | "include": [ 17 | "src/**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": true, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": false, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "sourceMap": true, 16 | "declaration": false, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "node", 19 | "importHelpers": true, 20 | "target": "ES2022", 21 | "module": "ES2022", 22 | "useDefineForClassFields": false 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------