JS frontend stuff

Move old ember frontend to properly named folder
Add vue based new frontend
This commit is contained in:
Melody Becker 2025-07-07 21:48:39 +02:00
parent 8947d97825
commit 88398334fe
Signed by: mstar
SSH key fingerprint: SHA256:vkXfS9FG2pVNVfvDrzd1VW9n8VJzqqdKQGljxxX8uK8
254 changed files with 837 additions and 0 deletions

View file

@ -0,0 +1,19 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 2
[*.hbs]
insert_final_newline = false
[*.{diff,md}]
trim_trailing_whitespace = false

View file

@ -0,0 +1,8 @@
{
/**
Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
*/
"isTypeScriptProject": true,
"pnpm": true
}

View file

@ -0,0 +1,14 @@
# unconventional js
/blueprints/*/files/
# compiled output
/declarations/
/dist/
# misc
/coverage/
!.*
.*/
# ember-try
/.node_modules.ember-try/

View file

@ -0,0 +1,55 @@
'use strict';
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
},
plugins: ['ember', '@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:ember/recommended',
'plugin:prettier/recommended',
],
env: {
browser: true,
},
rules: {},
overrides: [
// ts files
{
files: ['**/*.ts'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {},
},
// node files
{
files: [
'./.eslintrc.js',
'./.prettierrc.js',
'./.stylelintrc.js',
'./.template-lintrc.js',
'./ember-cli-build.js',
'./testem.js',
'./blueprints/*/index.js',
'./config/**/*.js',
'./lib/*/index.js',
'./server/**/*.js',
],
env: {
browser: false,
node: true,
},
extends: ['plugin:n/recommended'],
},
{
// test files
files: ['tests/**/*-test.{js,ts}'],
extends: ['plugin:qunit/recommended'],
},
],
};

View file

@ -0,0 +1,47 @@
name: CI
on:
push:
branches:
- main
- master
pull_request: {}
concurrency:
group: ci-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
lint:
name: "Lint"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- name: Install Dependencies
run: npm ci
- name: Lint
run: npm run lint
test:
name: "Test"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: 18
cache: npm
- name: Install Dependencies
run: npm ci
- name: Run Tests
run: npm test

25
frontend-old-ember/.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# compiled output
/dist/
/declarations/
# dependencies
/node_modules/
# misc
/.env*
/.pnp*
/.eslintcache
/coverage/
/npm-debug.log*
/testem.log
/yarn-error.log
# ember-try
/.node_modules.ember-try/
/npm-shrinkwrap.json.ember-try
/package.json.ember-try
/package-lock.json.ember-try
/yarn.lock.ember-try
# broccoli-debug
/DEBUG/

View file

@ -0,0 +1,13 @@
# unconventional js
/blueprints/*/files/
# compiled output
/dist/
# misc
/coverage/
!.*
.*/
# ember-try
/.node_modules.ember-try/

View file

@ -0,0 +1,12 @@
'use strict';
module.exports = {
overrides: [
{
files: '*.{js,ts}',
options: {
singleQuote: true,
},
},
],
};

View file

@ -0,0 +1,8 @@
# unconventional files
/blueprints/*/files/
# compiled output
/dist/
# addons
/.node_modules.ember-try/

View file

@ -0,0 +1,5 @@
'use strict';
module.exports = {
extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
};

View file

@ -0,0 +1,8 @@
'use strict';
module.exports = {
extends: 'recommended',
rules: {
'no-bare-strings': true,
},
};

View file

@ -0,0 +1,3 @@
{
"ignore_dirs": ["dist"]
}

View file

@ -0,0 +1,58 @@
# frontend-reactive
This README outlines the details of collaborating on this Ember application.
A short introduction of this app could easily go here.
## Prerequisites
You will need the following things properly installed on your computer.
- [Git](https://git-scm.com/)
- [Node.js](https://nodejs.org/) (with npm)
- [Ember CLI](https://cli.emberjs.com/release/)
- ~~Google Chrome~~ NO, Fuck off. Bad ember. No Chrome. [Firefox](https://www.mozilla.org/en-US/firefox/new/) does the job just fine. Or, if you really want to use Chrome, use [Ungoogled Chromium](https://github.com/ungoogled-software/ungoogled-chromium)
## Installation
- `git clone <repository-url>` this repository
- `cd frontend-reactive`
- `npm install`
## Running / Development
- `npm run start`
- Visit your app at [http://localhost:4200](http://localhost:4200).
- Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
### Code Generators
Make use of the many generators for code, try `ember help generate` for more details
### Running Tests
- `npm run test`
- `npm run test:ember -- --server`
### Linting
- `npm run lint`
- `npm run lint:fix`
### Building
- `npm exec ember build` (development)
- `npm run build` (production)
### Deploying
1. Build it (see point above)
2. Build the Go server
3. Now you have a binary with everything needed embedded into it ready to use
## Further Reading / Useful Links
- [ember.js](https://emberjs.com/)
- [ember-cli](https://cli.emberjs.com/release/)
- Development Browser Extensions
- [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
- [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)

View file

@ -0,0 +1,5 @@
- [ ] Bionic reading support
- [ ] [https://github.com/Gumball12/text-vide] for (theoretically) all strings
- [ ] [https://github.com/Born2Root/Fast-Font] as font
- [ ] Internationalisation with [https://ember-intl.github.io/ember-intl/docs/quickstart]
- [ ] Fix font sizing

View file

@ -0,0 +1,12 @@
import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from 'frontend-reactive/config/environment';
export default class App extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
}
loadInitializers(App, config.modulePrefix);

View file

@ -0,0 +1 @@
{{yield}}

View file

@ -0,0 +1,18 @@
<div class="account-header">
<Account::Header::Profilepicture @meta={{@data.icon}} />
<div class="account-header-">
</div>
</div>
<div class="account-header">
<Account::Header::Profilepicture @meta={{@data.icon}} />
<div class="account-header-text">
<Note::Formatter
@classes="note-user-displayname"
@content={{@data.displayname}}
@server={{@data.originServer}}
/>
<p class="note-user-displayname"></p>
<p class="note-user-handle">{{@data.originServer.id}}</p>
</div>
</div>

View file

@ -0,0 +1 @@
<img class="profile-picture" src="{{@meta.url}}" alt="{{@meta.altText}}" />

View file

@ -0,0 +1,3 @@
<div class="profile-overview">
<Account::Header @data={{@data}} />
</div>

View file

@ -0,0 +1,18 @@
<div class="auth-wrapper">
<div class="auth-username-wrapper">
<label>
Username
<Input
@type="text"
@value={{this.username}}
placeholder="Username"
/>
</label>
</div>
<div type="button" class="auth-button-login" {{on "click" this.startLogin}}>
Login
</div>
<div type="button" class="auth-button-register" {{on "click" this.startRegistration}}>
Register
</div>
</div>

View file

@ -0,0 +1,50 @@
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import '@simplewebauthn/browser';
import {
startAuthentication,
startRegistration,
} from '@simplewebauthn/browser';
import type AuthService from 'frontend-reactive/services/auth';
export interface AuthSignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class Auth extends Component<AuthSignature> {
@tracked username: string = '';
@tracked error: string | undefined;
//@tracked isLogin = true;
@service declare auth: AuthService;
@action async startLogin() {
try {
// TODO: Check if account exists and is alowed to login
this.auth.startLogin(this.username);
} catch (error: any) {
this.error = 'Error: ' + error.message;
}
}
@action async startRegistration() {
try {
// TODO: Check if handle is already taken
await this.auth.startRegistration(this.username);
// After registration, log in immediately to obtain a valid session token
// for the "post" registration data, such as email
await this.auth.startLogin(this.username);
// And after login,
} catch (error: any) {
this.error = 'Error: ' + error.message;
}
}
}

View file

@ -0,0 +1,12 @@
<div class="login-wrapper">
<label>
<Input
@type="text"
@value={{this.username}}
placeholder="Username"
/>
</label>
<div type="button" class="login-start-button" {{on "click" this.onLoginStart}}>
Login
</div>
</div>

View file

@ -0,0 +1,24 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export interface AuthLoginSignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class AuthLogin extends Component<AuthLoginSignature> {
@tracked username = '';
@action onLoginStart() {
console.log('Starting login for username ' + this.username);
// Check if username is approved for login
// If it is, continue with login
}
}

View file

@ -0,0 +1,83 @@
<div class="registration-form">
<h1 class="registration-form-username">username: {{this.args.username}}</h1>
<div class="registration-form-name-mail-wrapper">
<div class="registration-form-displayname-wrapper">
<label>
Displayname
<Input
@type="text"
@value={{this.displayname}}
placeholder="Displayname"
/>
</label>
</div>
<Util::MailEntry
@wrapper-classes="registration-form-mail-wrapper"
@input-classes="registration-form-mail-input"
@data={{this.mail}}
/>
</div>
<div class="registration-form-description-wrapper">
{{! TODO: Split into entry form on the left and live preview on the right }}
<label for="registration-description">
Description
</label>
<Textarea
id="registration-description"
@value={{this.description}}
placeholder="Account description"
/>
</div>
<fieldset class="registration-form-gender-wrapper">
<legend class="registration-form-gender-info">Add your preferred pronouns</legend>
<Util::StringArray
@list={{this.gender}}
@onNewElement={{this.genderAddedHandler}}
@onDeleteElement={{this.genderRemovedHandler}}
@wrapper-classes=""
@element-wrapper-classes=""
@element-classes=""
@remove-element-classes=""
@add-element-classes=""
/>
</fieldset>
<fieldset class="register-form-being-wrapper">
<legend class="registration-form-being-info">Select the type of being you are. Multiselect is possible</legend>
<Util::Multiselect
@elements={{this.beingTypes}}
@wrapper-class=""
@label-class=""
@input-classes=""
/>
</fieldset>
<fieldset class="register-form-default-post-mode-wrapper">
<legend class="registration-form-default-post-mode-info">Select the default mode for your posts</legend>
<Util::OneOfArray
@elements={{array "Public" "Local" "Followers" "Direct"}}
@selected={{this.defaultpostmode}}
@name="default-post-mode"
@required={{true}}
/>
</fieldset>
<div class="register-form-follow-approval-wrapper">
<label>
Require approval for follow requests
<Input
@type="checkbox"
name="Follow approval"
@checked={{this.args.followapproval}}
/>
</label>
</div>
<div class="register-form-indexable-wrapper">
<label>
Whether the account is indexable
<Input @type="checkbox" name="Indexable" @checked={{this.indexable}} />
</label>
</div>
<fieldset class="register-form-custom-fields-wrapper">
<legend>Custom fields</legend>
<Util::MapEdit @list={{this.customProperties}} />
</fieldset>
{{! TODO: Icon, Background, Banner, Bluesky toggle }}
</div>

View file

@ -0,0 +1,76 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import isValidMail from 'frontend-reactive/helpers/is-valid-mail';
export interface AuthPostRegistrationFormSignature {
// The arguments accepted by the component
Args: {
username: string;
};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class AuthPostRegistrationForm extends Component<AuthPostRegistrationFormSignature> {
@tracked displayname: string = this.args.username;
@tracked description: string = '';
@tracked gender: Array<{ value: string }> = [];
@tracked beingTypes: Array<{
name: string;
checked: boolean;
description: string;
}> = [
{
name: 'Human',
description: 'Human',
checked: true,
},
{
name: 'Cat',
description: 'Cat',
checked: false,
},
{
name: 'Fox',
description: 'Fox',
checked: false,
},
{
name: 'Dog',
description: 'Dog',
checked: false,
},
{
name: 'Robot',
description: 'Robot',
checked: false,
},
{
name: 'Doll',
description: 'Doll',
checked: false,
},
];
@tracked defaultpostmode: string = 'Public';
@tracked followapproval: boolean = false;
@tracked customProperties: Array<{ key: string; value: string }> = [];
@tracked indexable: boolean = true;
@tracked mail = { mail: '', valid: false };
@tracked enableBlueskyIntegration = false;
genderAddedHandler(newIndex: number) {
console.log('gender added');
}
genderRemovedHandler(removedIndex: number) {
console.log('gender removed');
}
@action test() {
console.log(this.mail);
}
}

View file

@ -0,0 +1 @@
{{yield}}

View file

@ -0,0 +1,14 @@
import Component from '@glimmer/component';
export interface AuthRegisterStartSignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class AuthRegisterStart extends Component<AuthRegisterStartSignature> {}

View file

@ -0,0 +1,5 @@
<div class="global-sidebar-general">
<div class="global-sidebar-general-feed">
</div>
</div>

View file

@ -0,0 +1,18 @@
<div class="note">
{{!-- TODO: figure out how to make the entire note clickable for opening with something like {{on "click" (fn this.openFullView)}} --}}
<Note::UserHeader
@displayname="{{@note.displayname}}"
@handle="@{{@note.username}}@{{@note.server}}"
@server="{{@note.servertype}}"
/>
<Note::Content @content="{{@note.content}}" />
<div class="note-timestamps-container">
{{#if @note.editedAt}}
<p class="note-timestamp" id="note-edited-timestamp">Edited: {{moment-format @note.editedAt "MMM DD, YYYY H:mm"}}</p>
{{/if}}
<p class="note-timestamp" id="note-created-timestamp">Posted: {{moment-format @note.createdAt "MMM DD, YYYY H:mm"}}</p>
</div>
{{!--<div class="separator-horizontal" />--}}
{{!-- TODO: Hardcoded values here, make them dynamic --}}
<Note::Interactions @boostCount="25" @totalLikeCount="300" @hasBoosted="true" @hasReacted="false" />
</div>

View file

@ -0,0 +1,34 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
export interface NoteSignature {
// The arguments accepted by the component
Args: {
isInTimeline: boolean;
note: {
content: string;
server: string;
username: string;
displayname: string;
createdAt: number;
editedAt?: number;
};
};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class Note extends Component<NoteSignature> {
@action
openFullView() {
if (this.args.isInTimeline) {
alert("Would have opened note's own view");
} else {
console.log("Alread in note specific view, can't open it again");
}
}
}

View file

@ -0,0 +1,19 @@
<div class="note-content">
<p class="note-content-text">{{this.visibleContent}}</p>
{{#if this.canExpand}}
{{#if this.collapsed}}
<div
type="button"
class="note-content-toggle"
{{on "click" this.expand}}
>{{t "note.expand"}}</div>
{{else}}
<div
type="button"
class="note-content-toggle"
{{on "click" this.collapse}}
>{{t "note.collapse"}}</div>
{{/if}}
{{/if}}
</div>

View file

@ -0,0 +1,58 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export interface NoteContentSignature {
// The arguments accepted by the component
Args: {
content: string;
preFormatted: boolean;
};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class NoteContent extends Component<NoteContentSignature> {
// Default to collapsed content
@tracked visibleContent =
this.args.content.length > 200 ? this.cutDownContent() : this.args.content;
@tracked collapsed = true;
@tracked canExpand = this.args.content.length > 200;
@action
expand() {
this.visibleContent = this.args.content;
this.collapsed = false;
}
@action
collapse() {
this.visibleContent =
this.args.content.length > 200
? this.cutDownContent()
: this.args.content;
this.collapsed = true;
}
cutDownContent(): string {
if (this.args.content.length > 200) {
const words = this.args.content.split(' ');
let outString = '';
for (const word of words) {
if (outString.length > 200) {
return outString + '...';
}
outString += word + ' ';
}
} else {
return this.args.content;
}
return '';
}
}

View file

@ -0,0 +1,46 @@
{{!-- TODO: Add translations --}}
<div class="resource-preload">
</div>
<div class="note-interactions-wrapper">
<div type="button" class="note-interactions-interaction-button" {{on "click" this.toggleBoost}}>
{{#if this.hasBoosted}}
<Svgs::ReloadOutline @class="note-interactions-interaction-icon"/>
{{else}}
<Svgs::ReloadColoured @class="note-interactions-interaction-icon"/>
{{/if}}
<p class="note-interactions-interaction-counter noselect">{{this.args.boostCount}}</p>
<EmberTooltip @text="Boost this note" @side="top-start"/>
</div>
<div class="note-interactions-interaction-button">
<div
type="button"
class="note-interactions-interactions-button-like"
aria-label="Like or unlike" {{on "click" this.toggleDefaultLike}}
>
{{#if this.hasReacted}}
<Svgs::HeartOutline @class="note-interactions-interaction-icon"/>
{{else}}
<Svgs::HeartFilled @class="note-interactions-interaction-icon"/>
{{/if}}
<p class="note-interactions-interaction-counter noselect">{{this.args.totalLikeCount}}</p>
<EmberTooltip @text="Like this note"/>
</div>
<div
class="note-interactions-interactions-button-custom"
aria-label="Send a custom reaction" type="button"
{{on "click" this.openCustomReactionSelector}}
>
<Svgs::PlusBlack @class="note-interactions-interaction-icon"/>
<EmberTooltip @text="Send a custom reaction"/>
</div>
<div
class="note-interactions-interactions-button-custom"
type="button"
aria-label="Expand existing reactions"
{{on "click" this.openAllReactions}}
>
<Svgs::ArrowDownBlack @class="note-interactions-interaction-icon"/>
<EmberTooltip @text="Expand reactions"/>
</div>
</div>
</div>

View file

@ -0,0 +1,53 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import isLandscape from 'frontend-reactive/helpers/isLandscape';
export interface NoteInteractionsSignature {
// The arguments accepted by the component
Args: {
boostCount: number;
totalLikeCount: number;
reactions: {
[key: string]: number;
};
hasBoosted: boolean;
hasReacted: boolean;
};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class NoteInteractions extends Component<NoteInteractionsSignature> {
@tracked hasBoosted = this.args.hasBoosted;
@tracked hasReacted = this.args.hasReacted;
@tracked expandReactions = false;
@action
toggleBoost() {
this.hasBoosted = !this.hasBoosted;
console.log('boosted', this.hasBoosted);
}
@action
toggleDefaultLike() {
this.hasReacted = !this.hasReacted;
console.log('reacted', this.hasReacted);
}
@action
openCustomReactionSelector() {
this.hasReacted = !this.hasReacted;
console.log('sent custom reaction', this.hasReacted);
}
@action
openAllReactions() {
console.log('Toggle all reactions overview');
this.expandReactions = !this.expandReactions;
}
}

View file

@ -0,0 +1,11 @@
<div class="note-user-header">
<div class="note-user-pfp">
<p>Pfp</p>
</div>
<div class="note-user-name-and-handle">
<p class="note-user-displayname">{{@displayname}}:{{@server}}</p>
<!--<Util::Formatter @classes="note-user-displayname" @content={{@displayname}} @server={{@server}}/>-->
<p class="note-user-displayname"></p>
<p class="note-user-handle">{{@handle}}</p>
</div>
</div>

View file

@ -0,0 +1 @@
{{yield}}

View file

@ -0,0 +1 @@
{{yield}}

View file

@ -0,0 +1 @@
{{yield}}

View file

@ -0,0 +1,9 @@
{{!--Source: https://www.iconpacks.net/free-icon/arrow-down-3101.html--}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve" class="{{@class}}">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path class="svg-black-white" d="M 90 24.25 c 0 -0.896 -0.342 -1.792 -1.025 -2.475 c -1.366 -1.367 -3.583 -1.367 -4.949 0 L 45 60.8 L 5.975 21.775 c -1.367 -1.367 -3.583 -1.367 -4.95 0 c -1.366 1.367 -1.366 3.583 0 4.95 l 41.5 41.5 c 1.366 1.367 3.583 1.367 4.949 0 l 41.5 -41.5 C 89.658 26.042 90 25.146 90 24.25 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

View file

@ -0,0 +1,9 @@
{{!--Source: https://www.iconpacks.net/free-icon/arrow-right-3098.html--}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve" class="{{@class}}">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path class="svg-black-white" d="M 24.25 90 c -0.896 0 -1.792 -0.342 -2.475 -1.025 c -1.367 -1.366 -1.367 -3.583 0 -4.949 L 60.8 45 L 21.775 5.975 c -1.367 -1.367 -1.367 -3.583 0 -4.95 c 1.367 -1.366 3.583 -1.366 4.95 0 l 41.5 41.5 c 1.367 1.366 1.367 3.583 0 4.949 l -41.5 41.5 C 26.042 89.658 25.146 90 24.25 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

View file

@ -0,0 +1,14 @@
{{!--Source: https://www.iconpacks.net/free-icon/rainbow-heart-4025.html--}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve" class="{{@class}}">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 22.353 72.146 c 6.131 5.349 13.628 10.42 22.647 14.93 c 9.019 -4.509 16.516 -9.58 22.647 -14.93 C 51.886 71.433 38.115 71.433 22.353 72.146 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(162,0,247); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 9.921 58.519 c 3.683 5.228 8.467 10.41 14.444 15.327 h 41.27 c 5.977 -4.917 10.761 -10.099 14.444 -15.327 C 54.42 56.482 35.58 56.482 9.921 58.519 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(1,104,248); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 2.868 45.313 c 1.777 4.764 4.411 9.652 7.991 14.49 h 68.283 c 3.58 -4.838 6.214 -9.726 7.991 -14.49 C 55.317 43.478 34.683 43.478 2.868 45.313 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(3,143,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 0.011 31.231 c 0.114 4.567 1.084 9.479 3.021 14.53 h 83.936 c 1.937 -5.051 2.907 -9.963 3.021 -14.53 C 55.571 27.636 34.429 27.636 0.011 31.231 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,245,1); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 3.063 16.796 c -2.176 4.208 -3.251 9.31 -3.037 14.923 h 89.947 c 0.214 -5.613 -0.861 -10.715 -3.037 -14.923 C 55.299 13.301 34.701 13.301 3.063 16.796 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(248,146,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 2.622 17.677 h 84.756 C 81.379 4.833 65.599 -0.363 45 12.219 C 24.401 -0.363 8.621 4.833 2.622 17.677 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(247,0,0); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

View file

@ -0,0 +1,10 @@
{{!--Source: https://www.iconpacks.net/free-icon/heart-2930.html--}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve" class="{{@class}}">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path class="svg-black-white" d="M 45 86.215 c -0.307 0 -0.613 -0.07 -0.895 -0.211 C 12.38 70.141 0.53 47.275 0.019 31.165 C -0.32 20.477 4.083 11.415 11.799 6.924 C 20.661 1.766 32.416 2.997 45 10.373 c 12.585 -7.375 24.338 -8.605 33.201 -3.449 c 7.716 4.491 12.119 13.553 11.78 24.241 c -0.511 16.11 -12.361 38.976 -44.087 54.839 C 45.613 86.145 45.307 86.215 45 86.215 z M 23.93 7.787 c -3.729 0 -7.139 0.86 -10.119 2.594 c -6.521 3.795 -10.09 11.324 -9.794 20.657 C 4.486 45.847 15.519 66.926 45 81.975 c 29.481 -15.049 40.514 -36.128 40.983 -50.937 c 0.296 -9.333 -3.273 -16.862 -9.795 -20.657 c -7.777 -4.528 -18.483 -3.095 -30.146 4.028 c -0.641 0.392 -1.446 0.392 -2.085 0 C 36.764 10.016 29.933 7.787 23.93 7.787 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path class="svg-black-white" d="M 11.953 39.751 c -0.855 0 -1.646 -0.553 -1.911 -1.413 c -2.26 -7.346 -0.825 -15.376 3.655 -20.458 c 3.241 -3.678 7.71 -5.331 12.273 -4.536 c 1.088 0.19 1.816 1.226 1.626 2.314 c -0.19 1.088 -1.23 1.818 -2.314 1.626 c -3.199 -0.557 -6.251 0.592 -8.585 3.24 c -3.58 4.062 -4.692 10.592 -2.832 16.638 c 0.325 1.056 -0.268 2.175 -1.324 2.5 C 12.346 39.722 12.148 39.751 11.953 39.751 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

View file

@ -0,0 +1,10 @@
{{!--Source: https://www.iconpacks.net/free-icon/plus-11960.html--}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve" class="{{@class}}">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path class="svg-black-white" d="M 45 90 c -2.761 0 -5 -2.238 -5 -5 V 5 c 0 -2.761 2.239 -5 5 -5 c 2.762 0 5 2.239 5 5 v 80 C 50 87.762 47.762 90 45 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path class="svg-black-white" d="M 85 50 H 5 c -2.761 0 -5 -2.238 -5 -5 c 0 -2.761 2.239 -5 5 -5 h 80 c 2.762 0 5 2.239 5 5 C 90 47.762 87.762 50 85 50 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

View file

@ -0,0 +1,10 @@
{{!--Source: https://www.iconpacks.net/free-icon/pink-plus-11966.html--}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve" class="{{@class}}">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 45 90 c -2.761 0 -5 -2.238 -5 -5 V 5 c 0 -2.761 2.239 -5 5 -5 c 2.762 0 5 2.239 5 5 v 80 C 50 87.762 47.762 90 45 90 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,49,250); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 85 50 H 5 c -2.761 0 -5 -2.238 -5 -5 c 0 -2.761 2.239 -5 5 -5 h 80 c 2.762 0 5 2.239 5 5 C 90 47.762 87.762 50 85 50 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,49,250); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

View file

@ -0,0 +1,10 @@
{{!--https://www.iconpacks.net/free-icon/arrows-reload-2848.html--}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve" class="{{@class}}">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path d="M 78.39 55.325 c -2.02 -0.89 -4.383 0.024 -5.273 2.047 C 68.207 68.512 57.17 75.709 45 75.709 c -12.697 0 -23.618 -7.746 -28.288 -18.76 l 1.808 -0.168 c 1.547 -0.144 2.871 -1.17 3.396 -2.633 c 0.524 -1.462 0.155 -3.096 -0.948 -4.19 l -7.859 -7.797 c -0.019 -0.019 -0.043 -0.031 -0.062 -0.049 c -0.18 -0.171 -0.373 -0.327 -0.583 -0.463 c -0.074 -0.048 -0.154 -0.083 -0.231 -0.125 c -0.165 -0.092 -0.332 -0.179 -0.51 -0.248 c -0.095 -0.036 -0.192 -0.061 -0.289 -0.09 c -0.166 -0.05 -0.332 -0.094 -0.506 -0.122 c -0.116 -0.019 -0.232 -0.027 -0.35 -0.035 C 10.482 41.022 10.39 41 10.292 41 c -0.068 0 -0.132 0.017 -0.2 0.02 c -0.057 0.003 -0.113 -0.008 -0.169 -0.003 c -0.066 0.006 -0.128 0.03 -0.193 0.04 c -0.166 0.024 -0.327 0.056 -0.485 0.1 c -0.109 0.03 -0.215 0.061 -0.32 0.099 c -0.16 0.059 -0.313 0.129 -0.462 0.207 c -0.095 0.049 -0.19 0.096 -0.281 0.153 c -0.15 0.094 -0.287 0.201 -0.422 0.313 c -0.077 0.063 -0.158 0.12 -0.23 0.19 C 7.37 42.269 7.231 42.437 7.1 42.612 c -0.031 0.041 -0.071 0.073 -0.1 0.115 l -0.025 0.036 c 0 0 -0.001 0.001 -0.001 0.001 l -6.266 9.072 c -0.883 1.278 -0.945 2.952 -0.161 4.294 c 0.722 1.233 2.041 1.979 3.452 1.979 c 0.123 0 0.247 -0.006 0.37 -0.017 l 4.078 -0.378 C 13.721 72.83 28.11 83.709 45 83.709 c 15.339 0 29.249 -9.071 35.437 -23.11 C 81.327 58.577 80.411 56.216 78.39 55.325 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,123,123); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path d="M 89.291 38.164 c 0.883 -1.278 0.946 -2.952 0.161 -4.293 c -0.784 -1.341 -2.271 -2.108 -3.821 -1.963 l -4.078 0.378 C 76.28 17.17 61.89 6.292 45 6.292 c -15.339 0 -29.249 9.071 -35.436 23.11 c -0.891 2.021 0.025 4.382 2.047 5.273 c 2.021 0.892 4.382 -0.025 5.273 -2.047 C 21.794 21.489 32.83 14.292 45 14.292 c 12.697 0 23.619 7.746 28.289 18.76 l -1.808 0.168 c -1.547 0.144 -2.871 1.169 -3.396 2.632 c -0.525 1.462 -0.155 3.096 0.947 4.19 l 7.859 7.798 c 0.061 0.061 0.134 0.105 0.199 0.162 c 0.129 0.113 0.256 0.229 0.399 0.325 c 0.083 0.055 0.174 0.093 0.261 0.142 c 0.14 0.079 0.276 0.163 0.425 0.225 c 0.104 0.043 0.214 0.066 0.322 0.1 c 0.14 0.045 0.277 0.098 0.424 0.128 C 79.179 48.972 79.443 49 79.709 49 c 0.122 0 0.246 -0.006 0.369 -0.017 c 0.068 -0.006 0.131 -0.031 0.198 -0.041 c 0.163 -0.023 0.321 -0.055 0.476 -0.098 c 0.111 -0.03 0.22 -0.062 0.327 -0.102 c 0.158 -0.058 0.308 -0.128 0.456 -0.204 c 0.097 -0.05 0.193 -0.097 0.285 -0.155 c 0.149 -0.093 0.286 -0.201 0.421 -0.312 c 0.077 -0.064 0.158 -0.12 0.23 -0.19 c 0.158 -0.152 0.298 -0.32 0.43 -0.496 c 0.03 -0.04 0.069 -0.072 0.098 -0.113 l 0.024 -0.035 c 0.001 -0.001 0.002 -0.002 0.002 -0.003 L 89.291 38.164 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(123,222,255); fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

View file

@ -0,0 +1,10 @@
{{!--Source: https://www.iconpacks.net/free-icon/reload-arrows-2846.html--}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="256" height="256" viewBox="0 0 256 256" xml:space="preserve" class="{{@class}}">
<defs>
</defs>
<g style="stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;" transform="translate(1.4065934065934016 1.4065934065934016) scale(2.81 2.81)" >
<path class="svg-black-white" d="M 79.133 57.837 c -1.012 -0.443 -2.19 0.014 -2.637 1.023 C 70.996 71.339 58.633 79.401 45 79.401 c -17.205 0 -31.5 -12.696 -34.01 -29.211 l 4.442 4.407 c 0.784 0.777 2.051 0.773 2.829 -0.012 c 0.778 -0.783 0.773 -2.05 -0.011 -2.828 l -8.242 -8.178 c -0.012 -0.012 -0.027 -0.019 -0.039 -0.031 c -0.088 -0.083 -0.181 -0.158 -0.283 -0.224 c -0.037 -0.024 -0.076 -0.041 -0.114 -0.062 c -0.083 -0.047 -0.168 -0.091 -0.258 -0.125 c -0.043 -0.017 -0.088 -0.028 -0.132 -0.041 c -0.088 -0.027 -0.176 -0.05 -0.269 -0.065 c -0.053 -0.008 -0.106 -0.012 -0.16 -0.016 C 8.7 43.012 8.651 43 8.598 43 c -0.034 0 -0.066 0.008 -0.1 0.01 c -0.028 0.001 -0.056 -0.004 -0.084 -0.002 c -0.034 0.003 -0.065 0.015 -0.099 0.02 c -0.081 0.012 -0.16 0.028 -0.238 0.049 c -0.055 0.015 -0.11 0.031 -0.163 0.051 c -0.079 0.029 -0.154 0.064 -0.228 0.102 c -0.048 0.025 -0.097 0.049 -0.143 0.078 c -0.074 0.046 -0.142 0.1 -0.209 0.155 c -0.039 0.032 -0.08 0.061 -0.116 0.096 c -0.079 0.075 -0.148 0.159 -0.214 0.246 c -0.015 0.021 -0.035 0.036 -0.05 0.058 L 6.94 43.881 c 0 0 0 0.001 -0.001 0.001 l -6.585 9.535 c -0.628 0.909 -0.4 2.154 0.509 2.782 c 0.347 0.24 0.743 0.354 1.135 0.354 c 0.635 0 1.259 -0.302 1.647 -0.863 l 3.389 -4.907 C 9.832 69.224 25.791 83.401 45 83.401 c 15.218 0 29.018 -9 35.156 -22.928 C 80.602 59.463 80.144 58.282 79.133 57.837 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
<path class="svg-black-white" d="M 89.646 36.583 c 0.628 -0.909 0.4 -2.155 -0.509 -2.782 c -0.909 -0.628 -2.156 -0.4 -2.782 0.509 l -3.389 4.907 C 80.168 20.776 64.209 6.598 45 6.598 c -15.218 0 -29.017 9 -35.156 22.928 c -0.445 1.011 0.013 2.191 1.023 2.637 c 1.01 0.446 2.192 -0.012 2.637 -1.023 C 19.004 18.661 31.367 10.598 45 10.598 c 17.204 0 31.499 12.695 34.009 29.21 l -4.441 -4.407 c -0.785 -0.778 -2.05 -0.773 -2.829 0.011 c -0.777 0.784 -0.772 2.05 0.011 2.829 l 8.236 8.171 c 0.001 0.001 0.001 0.001 0.002 0.002 l 0.005 0.005 c 0.022 0.022 0.049 0.038 0.072 0.058 c 0.073 0.066 0.146 0.13 0.227 0.185 c 0.039 0.026 0.083 0.044 0.124 0.067 c 0.072 0.041 0.142 0.084 0.219 0.116 c 0.05 0.021 0.104 0.031 0.155 0.048 c 0.072 0.023 0.142 0.05 0.217 0.065 c 0.129 0.026 0.261 0.04 0.394 0.04 c 0.062 0 0.123 -0.003 0.185 -0.009 c 0.032 -0.003 0.062 -0.015 0.094 -0.019 c 0.084 -0.012 0.166 -0.028 0.246 -0.05 c 0.054 -0.015 0.106 -0.03 0.158 -0.049 c 0.08 -0.029 0.156 -0.064 0.231 -0.103 c 0.048 -0.025 0.096 -0.049 0.143 -0.077 c 0.073 -0.046 0.14 -0.098 0.206 -0.153 c 0.04 -0.033 0.082 -0.062 0.119 -0.099 c 0.078 -0.075 0.147 -0.158 0.212 -0.245 c 0.016 -0.021 0.036 -0.037 0.051 -0.059 l 0.013 -0.018 c 0 0 0 -0.001 0.001 -0.001 L 89.646 36.583 z" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill-rule: nonzero; opacity: 1;" transform=" matrix(1 0 0 1 0 0) " stroke-linecap="round" />
</g>
</svg>

View file

@ -0,0 +1,6 @@
<div class="timeline">
{{#each @notes as |note|}}
<Note @isInTimeline="true" @note={{note}}/>
<hr class="timeline-separator">
{{/each}}
</div>

View file

@ -0,0 +1 @@
{{yield}}

View file

@ -0,0 +1,16 @@
<div class="{{@classes}}">
{{#if (equals @server "mastodon")}}
<Note::Formatter::Mastodon>{{@content}}Masto</Note::Formatter::Mastodon>
{{else if (equals @server "misskey")}}
<Note::Formatter::Misskey>{{@content}}Misskey</Note::Formatter::Misskey>
{{else if (equals @server "akoma")}}
<Note::Formatter::Akoma>{{@content}}Akoma</Note::Formatter::Akoma>
{{else if (equals @server "linstrom")}}
<Note::Formatter::Linstrom>{{@content}}Linstrom</Note::Formatter::Linstrom>
{{else if (equals @server "wafrn")}}
<Note::Formatter::Wafrn>{{@content}}Wafrn</Note::Formatter::Wafrn>
{{else}}
<Note::Formatter::Linstrom
>{{@content}}Unkown:{{@server}}</Note::Formatter::Linstrom>
{{/if}}
</div>

View file

@ -0,0 +1 @@
<p>{{@content}}</p>

View file

@ -0,0 +1,14 @@
import Component from '@glimmer/component';
export interface NoteFormatterAkomaSignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class NoteFormatterAkoma extends Component<NoteFormatterAkomaSignature> {}

View file

@ -0,0 +1,2 @@
<p>{{@content}}</p>
{{yield}}

View file

@ -0,0 +1,14 @@
import Component from '@glimmer/component';
export interface NoteFormatterLinstromSignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class NoteFormatterLinstrom extends Component<NoteFormatterLinstromSignature> {}

View file

@ -0,0 +1,2 @@
<p>{{@content}}</p>
{{yield}}

View file

@ -0,0 +1,14 @@
import Component from '@glimmer/component';
export interface NoteFormatterMastodonSignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class NoteFormatterMastodon extends Component<NoteFormatterMastodonSignature> {}

View file

@ -0,0 +1,2 @@
<p>{{@content}}</p>
{{yield}}

View file

@ -0,0 +1,14 @@
import Component from '@glimmer/component';
export interface NoteFormatterMisskeySignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class NoteFormatterMisskey extends Component<NoteFormatterMisskeySignature> {}

View file

@ -0,0 +1,2 @@
<p>{{@content}}</p>
{{yield}}

View file

@ -0,0 +1,14 @@
import Component from '@glimmer/component';
export interface NoteFormatterWafrnSignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class NoteFormatterWafrn extends Component<NoteFormatterWafrnSignature> {}

View file

@ -0,0 +1,18 @@
<div class="mail-entry {{@wrapper-classes}}">
<label>
Email
{{!--<div class="filling-spacer"/>--}}
<Input
class="mail-input {{if this.mailOk "mail-input-ok" "mail-input-error"}} {{@input-classes}}"
@type="text"
@value={{this.args.data.mail}}
placeholder="Email address"
{{on "change" this.checkMail}}
/>
</label>
{{#if this.mailOk}}
<p class="mail-ok mail-status">&#10004;</p>
{{else}}
<p class="mail-error mail-status">X</p>
{{/if}}
</div>

View file

@ -0,0 +1,27 @@
import { action } from '@ember/object';
import { map } from '@ember/object/computed';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
const re = /.+@\S+\.\S+/;
export interface UtilMailEntrySignature {
// The arguments accepted by the component
Args: {
data: { mail: string; valid: boolean };
};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class UtilMailEntry extends Component<UtilMailEntrySignature> {
@tracked mailOk = this.args.data.valid;
@action checkMail() {
this.args.data.valid = re.test(this.args.data.mail);
this.mailOk = this.args.data.valid;
}
}

View file

@ -0,0 +1,40 @@
<div class="{{@wrapper-classes}}">
{{#if @readonly}}
<ul>
{{#each this.args.list as |element|}}
<li>
<div class="string-array-element-wrapper">
<p class="{{@element-key-classes}}">{{element.key}}</p>
<p class="{{@element-value-classes}}">{{element.value}}</p>
</div>
</li>
{{/each}}
</ul>
{{else}}
<ul>
{{#each this.args.list as |element index|}}
<li>
<div class="string-array-element-wrapper">
<Input @type="text" @value={{element.key}} />
<Input @type="text" @value={{element.value}} />
<div
class="{{@remove-element-classes}}"
type="button"
{{on "click" (fn this.removeElement index)}}
>
X
</div>
</div>
</li>
{{/each}}
</ul>
<div
class="{{@add-element-classes}}"
type="button"
{{on "click" this.addElement}}
>
Add element
</div>
{{/if}}
</div>

View file

@ -0,0 +1,36 @@
import MutableArray from '@ember/array/mutable';
import { action } from '@ember/object';
import Component from '@glimmer/component';
export interface UtilMapEditSignature {
// The arguments accepted by the component
Args: {
list: MutableArray<{ key: string; value: string }>;
prefix: string;
onNewElement: (index: number) => void;
onDeleteElement: (index: number) => void;
};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class UtilMapEdit extends Component<UtilMapEditSignature> {
@action addElement() {
MutableArray.apply(this.args.list);
this.args.list.pushObject({ key: '', value: '' });
if (this.args.onNewElement)
this.args.onNewElement(this.args.list.length - 1);
}
@action removeElement(index: number) {
MutableArray.apply(this.args.list);
//let index = this.args.list.find((elem) => elem == content)
//let index = this.listCopy.findIndex((d) => d == content)
this.args.list.removeAt(index);
if (this.args.onDeleteElement) this.args.onDeleteElement(index);
}
}

View file

@ -0,0 +1,26 @@
<div class={{@wrapper-class}}>
{{#if @readonly}}
<ul>
{{#each this.args.elements as |element|}}
{{#if element.checked}}
<li>
<p>{{element.name}}</p>
</li>
{{/if}}
{{/each}}
</ul>
{{else}}
{{#each this.args.elements as |element|}}
<label class={{@label-class}}>
{{element.description}}
<Input
@type="checkbox"
name="{{element.name}}"
class="{{@input-classes}}"
@checked={{element.checked}}
{{on "change" this.onChange}}
/>
</label>
{{/each}}
{{/if}}
</div>

View file

@ -0,0 +1,21 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
export interface UtilMultiselectSignature {
// The arguments accepted by the component
Args: {
elements: Array<{ name: string; checked: boolean; description: string }>;
};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class UtilMultiselect extends Component<UtilMultiselectSignature> {
@action onChange() {
console.log(this.args.elements);
}
}

View file

@ -0,0 +1,16 @@
<div class="{{@wrapper-class}}">
{{#if @readonly}}
<p class="{{@element-classes}}">{{@selected}}</p>
{{else}}
{{#each @elements as |element index|}}
<RadioButton
@value="{{element}}"
@groupValue={{@selected}}
@name={{@name}}
@required={{@required}}
>
{{element}}
</RadioButton>
{{/each}}
{{/if}}
</div>

View file

@ -0,0 +1,38 @@
<div class="{{@wrapper-classes}}">
{{#if @readonly}}
<ul>
{{#each this.args.list as |element|}}
<p class="{{@readonly-element-classes}}">{{element.value}}</p>
{{/each}}
</ul>
{{else}}
<ul>
{{#each this.args.list as |element index|}}
<li>
<div class="string-array-element-wrapper {{@element-wrapper-classes}}">
<Input
class="{{@element-classes}}"
@type="text"
@value={{element.value}}
/>
<div
class="{{@remove-element-classes}}"
type="button"
{{on "click" (fn this.removeElement index)}}
>
X
</div>
</div>
</li>
{{/each}}
</ul>
<div
class="{{@add-element-classes}}"
type="button"
{{on "click" this.addElement}}
>
Add element
</div>
{{/if}}
</div>

View file

@ -0,0 +1,56 @@
import MutableArray from '@ember/array/mutable';
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
export interface UtilStringArraySignature {
// The arguments accepted by the component
Args: {
list: MutableArray<{ value: string }>;
prefix: string;
onNewElement: (index: number) => void;
onDeleteElement: (index: number) => void;
};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class UtilStringArray extends Component<UtilStringArraySignature> {
@action addElement() {
MutableArray.apply(this.args.list);
this.args.list.pushObject({ value: '' });
if (this.args.onNewElement)
this.args.onNewElement(this.args.list.length - 1);
}
@action removeElement(index: number) {
MutableArray.apply(this.args.list);
//let index = this.args.list.find((elem) => elem == content)
//let index = this.listCopy.findIndex((d) => d == content)
this.args.list.removeAt(index);
if (this.args.onDeleteElement) this.args.onDeleteElement(index);
}
transformArrayIntoUsable(arr: Array<string>): { [key: number]: string } {
const out: { [key: number]: string } = {};
const tmp = arr.map((elem: string, index: number) => {
out[index] = elem;
return elem;
});
return out;
}
countElemsInObj(obj: any): number {
let count = 0;
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) ++count;
}
return count;
}
}

View file

@ -0,0 +1,14 @@
/**
* Type declarations for
* import config from 'frontend-reactive/config/environment'
*/
declare const config: {
environment: string;
modulePrefix: string;
podModulePrefix: string;
locationType: 'history' | 'hash' | 'none';
rootURL: string;
APP: Record<string, unknown>;
};
export default config;

View file

@ -0,0 +1,29 @@
export default {
time: {
hhmmss: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
},
date: {
hhmmss: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
},
number: {
compact: {
notation: 'compact',
},
EUR: {
style: 'currency',
currency: 'EUR',
},
USD: {
style: 'currency',
currency: 'USD',
},
},
};

View file

View file

@ -0,0 +1,7 @@
import { helper } from '@ember/component/helper';
export default helper(function equals(args) {
if (args.length != 2) return false;
console.log(args[0], args[1]);
return args[0] == args[1];
});

View file

@ -0,0 +1,24 @@
import { helper } from '@ember/component/helper'
class Token {
declare token: string
declare elements: Array<string | Token>
public constructor(token: string) {
this.token = token
this.elements = new Array()
}
}
export default helper(function formatter(
positional: Array<string>,
named: {
matchers: Array<{ match: RegExp; replace: string }>
},
): string {
let out = positional[0] ?? ''
named.matchers.forEach((x) => {
out = out.replaceAll(x.match, x.replace)
})
return out
})

View file

@ -0,0 +1,14 @@
import { helper } from '@ember/component/helper';
const re = /.+@\S+\.\S+/;
// Helper to check if a given email is *probably* valid
// Ofc, the only surefire way to check if an email exists is to send a test mail to it.
// This sending is expensive however, and thus some mostly sane defaults can be checked for
// beforehand. "Bananentürkis" for example is obviously not a valid address
export default helper(function isValidMail(positional: string[] /*, named*/) {
for (const mail of positional) {
if (!re.test(mail)) return false;
}
return true;
});

View file

@ -0,0 +1,6 @@
export default function isLandscape(): boolean {
return (
Math.min(screen.availHeight, window.innerHeight) <
Math.min(screen.availWidth, window.innerWidth)
);
}

View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FrontendReactive</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for "head"}}
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/frontend-reactive.css">
{{content-for "head-footer"}}
</head>
<body>
{{content-for "body"}}
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/frontend-reactive.js"></script>
{{content-for "body-footer"}}
</body>
</html>

View file

View file

@ -0,0 +1,36 @@
import Model, { attr, hasMany, type AsyncHasMany } from '@ember-data/model';
import type CustomAccountFieldModel from './custom-account-field';
import type MediaMetadataModel from './media-metadata';
import type OriginServer from './origin-server';
export default class Account extends Model {
@attr declare createdAt: Date;
@attr declare updatedAt: Date;
@attr declare username: string;
@attr declare originServer: OriginServer;
@attr declare originServerId: number;
@attr declare displayName: string;
@hasMany('custom-account-field')
declare customFields: AsyncHasMany<CustomAccountFieldModel>;
@attr declare customFieldIds: Array<number>;
@attr declare isBot: boolean;
@attr declare description: string;
@attr declare icon: MediaMetadataModel;
@attr declare iconId: string;
@attr declare banner: MediaMetadataModel;
@attr declare bannerId: string;
@attr declare background: MediaMetadataModel;
@attr declare backgroundId: string;
@attr declare relationIds: Array<number>;
@attr declare indexable: boolean;
@attr declare restrictedFollow: boolean;
@attr declare identifiesAs: Array<string>;
@attr declare pronouns: Array<string>;
@attr declare roles: Array<string>;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
account: Account;
}
}

View file

@ -0,0 +1,16 @@
import Model, { attr } from '@ember-data/model';
export default class CustomAccountField extends Model {
@attr declare createdAt: Date;
@attr declare updatedAt: Date;
@attr declare key: string;
@attr declare value: string;
@attr declare verified?: boolean;
@attr declare belongsToId: string;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
customAccountField: CustomAccountField;
}
}

View file

@ -0,0 +1,17 @@
import Model, { attr } from '@ember-data/model';
import type MediaMetadataModel from './media-metadata';
import type OriginServer from './origin-server';
export default class Emote extends Model {
@attr declare metadataId: string;
@attr declare metadata: MediaMetadataModel;
@attr declare name: string;
@attr declare serverId: number;
@attr declare server: OriginServer;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
emote: Emote;
}
}

View file

@ -0,0 +1,18 @@
import Model, { attr } from '@ember-data/model';
export default class MediaMetadata extends Model {
@attr declare createdAt: Date;
@attr declare updatedAt: Date;
@attr declare isRemote: boolean;
@attr declare url: string;
@attr declare mimeType: string;
@attr declare name: string;
@attr declare altText: string;
@attr declare blurred: boolean;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
mediaMetadata: MediaMetadata;
}
}

View file

@ -0,0 +1,32 @@
import Model, { attr, hasMany, type AsyncHasMany } from '@ember-data/model';
import type AccountModel from './account';
import type MediaMetadataModel from './media-metadata';
import type OriginServer from './origin-server';
export default class Note extends Model {
@attr declare content: string;
@attr declare originServer: OriginServer;
@attr declare originServerId: number;
@attr declare reactionCount: number;
@attr declare createdAt: Date;
@attr declare updatedAt: Date;
@attr declare author: AccountModel;
@attr declare authorId: string;
@attr declare contentWarning?: string;
@attr declare inReplyToId?: string;
@attr declare quotesId?: string;
@attr declare emoteIds?: Array<string>;
@hasMany('media-metadata')
declare attachments: AsyncHasMany<MediaMetadataModel>;
@attr declare attachmentIds: Array<string>;
@attr declare accessLevel: number;
@hasMany('account') declare pings?: AsyncHasMany<AccountModel>;
@attr declare pingIds?: Array<string>;
@attr declare reactionIds: Array<number>;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
note: Note;
}
}

View file

@ -0,0 +1,14 @@
import Model, { attr } from '@ember-data/model';
export default class OriginServer extends Model {
@attr() declare serverType: string;
@attr() declare name: string;
@attr() declare iconUrl: string;
@attr() declare isSelf: boolean;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
originServer: OriginServer;
}
}

View file

@ -0,0 +1,15 @@
import Model, { attr } from '@ember-data/model';
import type EmoteModel from './emote';
export default class Reaction extends Model {
@attr declare noteId: string;
@attr declare reactorId: string;
@attr declare emoteId: number;
@attr declare emote: EmoteModel;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
reaction: Reaction;
}
}

View file

@ -0,0 +1,16 @@
import Model, { attr } from '@ember-data/model';
export default class Relation extends Model {
@attr declare createdAt: Date;
@attr declare updatedAt: Date;
@attr declare fromId: string;
@attr declare toId: string;
@attr declare requested: boolean;
@attr declare accepted: boolean;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
relation: Relation;
}
}

View file

@ -0,0 +1,64 @@
// Code generated by cmd/RolesApiTypeGenerator DO NOT EDIT.
// If you need to refresh the content, run go generate again
import Model, { attr } from '@ember-data/model';
export default class Role extends Model {
@attr() declare createdAt: Date;
@attr() declare updatedAt: Date;
@attr() declare name: string;
@attr() declare priority: number;
@attr() declare isUserRole: boolean;
@attr() declare isBuiltIn: boolean;
@attr() declare canSendCustomReactions?: boolean;
@attr() declare autoCwPostsText?: string;
@attr() declare withholdNotesRegexes?: Array<string>;
@attr() declare canAssignRoles?: boolean;
@attr() declare canSupressInteractionsBetweenUsers?: boolean;
@attr() declare canSendCustomEmotes?: boolean;
@attr() declare canOverwriteDisplayNames?: boolean;
@attr() declare canManageCustomEmotes?: boolean;
@attr() declare canDeleteNotes?: boolean;
@attr() declare canManageAvatarDecorations?: boolean;
@attr() declare canManageAds?: boolean;
@attr() declare blockedUsers?: Array<string>;
@attr() declare hasMentionCountLimit?: boolean;
@attr() declare disallowInteractionsWith?: Array<string>;
@attr() declare withholdNotesBasedOnRegex?: boolean;
@attr() declare fullAdmin?: boolean;
@attr() declare canSendAnnouncements?: boolean;
@attr() declare autoNsfwMedia?: boolean;
@attr() declare scanCreatedPublicNotes?: boolean;
@attr() declare scanCreatedPrivateNotes?: boolean;
@attr() declare canSendFollowerOnlyNotes?: boolean;
@attr() declare canSendPrivateNotes?: boolean;
@attr() declare canIncludeSurvey?: boolean;
@attr() declare canChangeDisplayName?: boolean;
@attr() declare canLogin?: boolean;
@attr() declare canAffectOtherAdmins?: boolean;
@attr() declare autoCwPosts?: boolean;
@attr() declare scanCreatedFollowerOnlyNotes?: boolean;
@attr() declare canViewDeletedNotes?: boolean;
@attr() declare mentionLimit?: number;
@attr() declare withholdNotesForManualApproval?: boolean;
@attr() declare canConfirmWithheldNotes?: boolean;
@attr() declare canSendMedia?: boolean;
@attr() declare canSendPublicNotes?: boolean;
@attr() declare canSendReplies?: boolean;
@attr() declare canQuote?: boolean;
@attr() declare canIncludeLinks?: boolean;
@attr() declare canRecoverDeletedNotes?: boolean;
@attr() declare canMentionOthers?: boolean;
@attr() declare scanCreatedLocalNotes?: boolean;
@attr() declare canSendLocalNotes?: boolean;
@attr() declare canBoost?: boolean;
@attr() declare canFederateFedi?: boolean;
@attr() declare canFederateBsky?: boolean;
@attr() declare canSubmitReports?: boolean;
}
declare module 'ember-data/types/registries/model' {
export default interface ModelRegistry {
role: Role;
}
}

View file

@ -0,0 +1,14 @@
import EmberRouter from '@ember/routing/router';
import config from 'frontend-reactive/config/environment';
export default class Router extends EmberRouter {
location = config.locationType;
rootURL = config.rootURL;
}
Router.map(function () {
this.route('about');
this.route('registerform');
this.route('auth');
this.route('testing');
});

View file

View file

@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class AboutRoute extends Route {}

View file

@ -0,0 +1,4 @@
import Route from '@ember/routing/route'
import { type Registry as Services, service } from '@ember/service'
export default class ApplicationRoute extends Route {}

View file

@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
import { tracked } from '@glimmer/tracking';
export default class AuthRoute extends Route {}

View file

@ -0,0 +1,54 @@
import Route from '@ember/routing/route'
import { type Registry as Services, service } from '@ember/service'
export default class IndexRoute extends Route {
@service declare intl: Services['intl']
beforeModel() {
this.intl.setLocale(['en-us'])
}
model() {
console.log('root route loaded')
return {
notes: [
{
displayname: 'alice',
username: 'bob',
server: 'example.com',
content: 'lorem ipsum',
createdAt: Date.now() - 360000,
editedAt: Date.now() - 60000,
servertype: 'mastodon',
},
{
displayname: 'Melody',
username: 'mstar',
server: 'woem.men',
content:
'Grapple keel reef fathom haul wind bilge rat swing the lead belay line pink. Man-of-war mizzenmast killick lookout yo-ho-ho Sail ho gabion careen sutler stern. Draught wherry lookout schooner prow hail-shot spanker Letter of Marque lateen sail strike colors.\n\nLad heave to topgallant scallywag scuppers Spanish Main poop deck spike hulk broadside. Snow take a caulk hornswaggle gaff swab quarter lugger spanker bilge provost. Man-of-war measured fer yer chains lugger cable loaded to the gunwalls prow piracy snow doubloon furl.\n\nDead men tell no tales jib chase guns gunwalls Gold Road smartly nipperkin topsail bilge water Pirate Round. Gaff gunwalls bilged on her anchor bilge water scourge of the seven seas parley ho sheet chase guns squiffy. Scuppers fathom ho quarter gally heave to yardarm coxswain red ensign pink.',
createdAt: Date.now() - 3600,
servertype: 'linstrom',
},
{
displayname: 'alice',
username: 'bob',
server: 'example.com',
content: 'lorem ipsum',
createdAt: Date.now() - 360000,
editedAt: Date.now() - 60000,
servertype: 'wafrn',
},
{
displayname: 'Melody',
username: 'mstar',
server: 'woem.men',
content:
'Grapple keel reef fathom haul wind bilge rat swing the lead belay line pink. Man-of-war mizzenmast killick lookout yo-ho-ho Sail ho gabion careen sutler stern. Draught wherry lookout schooner prow hail-shot spanker Letter of Marque lateen sail strike colors.\n\nLad heave to topgallant scallywag scuppers Spanish Main poop deck spike hulk broadside. Snow take a caulk hornswaggle gaff swab quarter lugger spanker bilge provost. Man-of-war measured fer yer chains lugger cable loaded to the gunwalls prow piracy snow doubloon furl.\n\nDead men tell no tales jib chase guns gunwalls Gold Road smartly nipperkin topsail bilge water Pirate Round. Gaff gunwalls bilged on her anchor bilge water scourge of the seven seas parley ho sheet chase guns squiffy. Scuppers fathom ho quarter gally heave to yardarm coxswain red ensign pink.',
createdAt: Date.now() - 3600,
servertype: 'unknown',
},
],
}
}
}

View file

@ -0,0 +1,9 @@
import Route from '@ember/routing/route';
export default class RegisterFormRoute extends Route {
async model() {
return {
list: [{ value: 'one' }, { value: 'two' }],
};
}
}

View file

@ -0,0 +1,3 @@
import Route from '@ember/routing/route';
export default class TestingRoute extends Route {}

View file

@ -0,0 +1,143 @@
import Service from '@ember/service';
import {
startAuthentication,
startRegistration,
} from '@simplewebauthn/browser';
export default class AuthService extends Service {
async startLogin(username: string) {
// Get login options from your server. Here, we also receive the challenge.
const response = await fetch('/webauthn/passkey/loginBegin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username }),
});
// Check if the login options are ok.
if (!response.ok) {
const msg = await response.json();
throw new Error('Failed to get login options from server: ' + msg);
}
// Convert the login options to JSON.
const options = await response.json();
// This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
// A new assertionResponse is created. This also means that the challenge has been signed.
const assertionResponse = await startAuthentication(options.publicKey);
// Send assertionResponse back to server for verification.
const verificationResponse = await fetch('/webauthn/passkey/loginFinish', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(assertionResponse),
});
const msg = await verificationResponse.json();
if (verificationResponse.ok) {
return;
} else {
throw new Error(
'Bad response code: ' +
verificationResponse.status +
'. Content: ' +
msg,
);
}
}
async startRegistration(username: string) {
// Get registration options from your server. Here, we also receive the challenge.
const response = await fetch('/webauthn/passkey/registerBegin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username }),
});
// Check if the registration options are ok.
if (!response.ok) {
const msg = await response.json();
throw new Error(
'User already exists or failed to get registration options from server: ' +
msg,
);
}
// Convert the registration options to JSON.
const options = await response.json();
console.log('registration start', options);
// This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
// A new attestation is created. This also means a new public-private-key pair is created.
const attestationResponse = await startRegistration(options.publicKey);
console.log('Attempting to complete registration', attestationResponse);
// Send attestationResponse back to server for verification and storage.
const verificationResponse = await fetch(
'/webauthn/passkey/registerFinish',
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(attestationResponse),
},
);
const msg = await verificationResponse.json();
if (verificationResponse.ok) {
return;
} else {
throw new Error(
'Bad response code: ' +
verificationResponse.status +
'. Content: ' +
msg,
);
}
}
// Check if the client is currently logged in
// Happens via caling an endpoint that's only available if logged in but doesn't return anything other than "ok"
async getLoginState(): Promise<boolean> {
try {
const response = await fetch('/api/test-auth');
return response.status >= 200 && response.status < 300;
} catch (error: any) {
return false;
}
}
// Check if a given username exist on the server
// Note: The server enforces this check itself during both registration and login
// but provides the information too so that frontends can provide a better UX
async doesUsernameExist(username: string): Promise<boolean> {
// TODO: Make API call to check if username/handle is already in use
return true;
}
// Check if a given username is allowed to log in.
// This includes a check for the existence of the username in the first place
// A username may not log in for various reasons, two of which are the account not being approved yet
// or the account being barred login from an admin
// Note: The server enforces this check itself during login. However, it also provides an API endpoint
// for performing this check to allow frontends to have a better UX
async canUsernameLogin(username: string): Promise<boolean> {
// Can't login into a non-existing account
if (!(await this.doesUsernameExist(username))) return false;
// TODO: Make API call to check if username is allowed to login
return true;
}
}
// Don't remove this declaration: this is what enables TypeScript to resolve
// this service using `Owner.lookup('service:auth')`, as well
// as to check when you pass the service name as an argument to the decorator,
// like `@service('auth') declare altName: AuthService;`.
declare module '@ember/service' {
interface Registry {
auth: AuthService;
}
}

View file

@ -0,0 +1,16 @@
/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */
/* Note: CSS is fucking stupid. It applies styles not in the order classes are set on an element,
* but in the order they appear in the css files */
/* @import url("debug.css"); */
@import url("fonts.css");
@import url("colors.css");
@import url("util.css");
@import url("util/stringArray.css");
@import url("util/mailEntry.css");
@import url("svgs.css");
@import url("notes.css");
@import url("timeline.css");
@import url("auth.css");
@import url("auth/registerForm.css");

View file

Some files were not shown because too many files have changed in this diff Show more