Compare commits
6 commits
0dd903a4eb
...
7ed8ebf6e5
Author | SHA1 | Date | |
---|---|---|---|
7ed8ebf6e5 | |||
876e221400 | |||
e3586f4eec | |||
563541d0e6 | |||
00277741a8 | |||
6f446fd871 |
12 changed files with 315 additions and 56 deletions
3
src/img/icons/cross.svg
Normal file
3
src/img/icons/cross.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32">
|
||||
<path d="M20.763 7.627a2 2 0 1 1 2.908 2.746l-5.236 5.545 5.547 5.236.141.149a2 2 0 0 1-2.887 2.76l-5.546-5.237-5.236 5.547a2 2 0 1 1-2.908-2.746l5.236-5.546-5.546-5.235-.142-.149a2 2 0 0 1 2.731-2.893l.157.133 5.545 5.236 5.236-5.546Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 329 B |
3
src/img/icons/tick.svg
Normal file
3
src/img/icons/tick.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 32 32">
|
||||
<path d="M24.546 8.627a2 2 0 0 1 2.908 2.746l-.03.032-.03.032-.03.032-.03.032-.06.064-.06.063-.12.126c-.036.04-.02.023-.058.062l-.03.032-.059.062-.029.03-.059.063-.058.062-.03.03-.028.032-.116.122-.028.03-.03.032c-.097.103.04-.044-.057.06l-.453.48-.223.236-.11.117-.11.116-.856.907-.052.055-.053.056-.31.328-.051.055-.026.027-.026.027c-.114.122.064-.067-.05.054l-.026.027-.025.027-.051.055-.026.026-.025.027-.102.108-.05.052-.026.028-.024.026-.1.107-.051.052-.15.159-.024.026-.026.026-.049.053-.1.105-.023.026-.025.026-.025.027-.025.025-.097.104-.099.104-.024.026-.025.026-.146.155-.048.052-.049.05-.097.103-.049.052-.047.05-.097.103-.096.102c-.09.095.042-.045-.049.05l-.023.026-.024.025-.192.203-.023.026-.024.024-.024.026-.024.025-.38.403-.095.1-.095.1c-.04.043-.006.008-.047.05l-.023.026-.025.025-.093.1-.19.2-.023.025-.024.025-.046.05-.19.2-.047.05-.047.05-.19.2-.023.025-.023.025c-.084.089.036-.038-.048.05l-.047.05-.048.05a59.55 59.55 0 0 1-.094.1c-.104.11.057-.059-.047.051l-.048.05-.047.05-.023.025-.025.025c-.058.062.011-.01-.047.051-.042.046-.005.005-.048.05l-.094.1-.025.026-.023.025-.048.051-.048.05-.335.355-1.572 1.666-.102.107-.025.027-.025.027-.026.027-.025.026-.102.109-.026.027-.05.055-.027.026-.051.055-.026.027-.026.027-.103.11c-.033.035-.02.02-.052.056l-.053.054-.025.028-.053.055-.026.029-.105.11c-.052.056 0 .002-.053.056l-.026.029-.026.027-.027.028-.027.029-.053.056-.106.113c-.066.07.012-.014-.054.056l-.026.029-.028.028-.107.114c-.076.08-.032.035-.108.116l-.11.115-.11.116-.027.03-.083.087-.222.236-.226.238-.113.12-.03.031-.084.091-.03.03-.028.03-.03.03-.027.032-.06.06c-.123.132.067-.069-.057.062l-.029.031-.03.03-.028.032-.058.062-.03.03-.029.032-.03.031-.059.063-.06.062c-.09.097-.026.03-.117.126l-.24.255a2.002 2.002 0 0 1-2.91 0l-5.782-6.125-.133-.156a2 2 0 0 1 3.041-2.59l4.328 4.584.164-.173c.057-.06 0 .002.056-.058l.055-.06.054-.057.029-.029.027-.03.108-.114.028-.029.027-.03.027-.027.027-.03.054-.056.027-.029.027-.029.027-.028.027-.029.053-.056.027-.029.026-.028.054-.057.053-.055c.095-.1-.042.044.053-.057l.21-.223.21-.22.103-.11.05-.055.027-.027.05-.054.027-.028.051-.054.103-.108.203-.216.2-.212.1-.106.026-.026.024-.027.026-.026.024-.027.025-.026.05-.053.025-.026.098-.105.05-.052.049-.052.099-.104.39-.414c.107-.113-.01.012.097-.102.098-.103-.05.052.048-.052l.049-.05.095-.103.097-.102.048-.05.023-.026.025-.025.191-.203.024-.026.024-.024.023-.026.025-.025.38-.403.094-.1.095-.1.047-.05.024-.026.024-.025.047-.05.048-.05.188-.2.023-.025.025-.025.047-.05.047-.05.047-.05.094-.1.048-.05.047-.05.188-.2.025-.025.023-.025.047-.05.048-.05.047-.05.095-.1c.067-.072-.02.02.046-.051.05-.052 0 .002.048-.05l.048-.05.024-.025.023-.025c.054-.058.04-.044.095-.101l.096-.1.023-.026.024-.025.047-.051.025-.025.023-.025.096-.101.023-.026.025-.025.191-.203 1.572-1.666.026-.027.025-.026.152-.161.103-.109.05-.054.027-.028.026-.026.206-.22.052-.055.052-.054.052-.056.264-.278.212-.226.053-.056.054-.057.055-.058.053-.056.109-.115.109-.116.055-.058.027-.029.027-.029.11-.117.223-.236.454-.48c.046-.05.01-.011.057-.06l.058-.062.115-.122.03-.031.029-.03.087-.094.03-.03.117-.126.06-.062.029-.031.03-.033.06-.062.06-.064.03-.032.029-.031.06-.064.06-.064Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.2 KiB |
|
@ -23,6 +23,7 @@
|
|||
"navigation": {
|
||||
"timeline": "Timeline",
|
||||
"notifications": "Notifications",
|
||||
"follow_requests": "Follow requests",
|
||||
"explore": "Explore",
|
||||
"lists": "Lists",
|
||||
|
||||
|
@ -37,6 +38,10 @@
|
|||
"back": "Back"
|
||||
},
|
||||
|
||||
"follow_requests": {
|
||||
"none": "no follow requests to action right now!"
|
||||
},
|
||||
|
||||
"timeline": {
|
||||
"home": "Home",
|
||||
"local": "Local",
|
||||
|
|
|
@ -176,6 +176,66 @@ export async function getNotifications(host, token, min_id, max_id, limit, types
|
|||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/follow_requests
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} min_id - If provided, only shows follow requests since this ID.
|
||||
* @param {string} max_id - If provided, only shows follow requests before this ID.
|
||||
* @param {string} limit - The maximum number of follow requests to retrieve (default 40, max 80).
|
||||
*/
|
||||
export async function getFollowRequests(host, token, since_id, max_id, limit) {
|
||||
let url = `https://${host}/api/v1/follow_requests`;
|
||||
|
||||
let params = new URLSearchParams();
|
||||
if (since_id) params.append("since_id", since_id);
|
||||
if (max_id) params.append("max_id", max_id);
|
||||
if (limit) params.append("limit", limit);
|
||||
const params_string = params.toString();
|
||||
if (params_string) url += '?' + params_string;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/follow_requests/:account_id/authorize
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} account_id - The account ID of the follow request to accept
|
||||
*/
|
||||
export async function acceptFollowRequest(host, token, account_id) {
|
||||
let url = `https://${host}/api/v1/follow_requests/${account_id}/authorize`;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/follow_requests/:account_id/reject
|
||||
* @param {string} host - The domain of the target server.
|
||||
* @param {string} token - The application token.
|
||||
* @param {string} account_id - The account ID of the follow request to reject
|
||||
*/
|
||||
export async function rejectFollowRequest(host, token, account_id) {
|
||||
let url = `https://${host}/api/v1/follow_requests/${account_id}/reject`;
|
||||
|
||||
const data = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
}).then(res => res.json());
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/timelines/{timeline}
|
||||
* @param {string} host - The domain of the target server.
|
||||
|
|
28
src/lib/followRequests.js
Normal file
28
src/lib/followRequests.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { server } from './client/server.js';
|
||||
import { writable } from "svelte/store";
|
||||
import * as api from "./api.js";
|
||||
import { app } from './client/app.js';
|
||||
import { get } from 'svelte/store';
|
||||
import { parseAccount } from './account.js';
|
||||
|
||||
// Cache for all requests
|
||||
export let followRequests = writable();
|
||||
|
||||
/**
|
||||
* Gets all follow requests
|
||||
* @param {boolean} force
|
||||
*/
|
||||
export async function fetchFollowRequests(force) {
|
||||
// if already cached, return for now
|
||||
if(!get(followRequests) && !force) return;
|
||||
|
||||
let newReqs = await api.getFollowRequests(
|
||||
get(server).host,
|
||||
get(app).token
|
||||
);
|
||||
|
||||
// parse accounts
|
||||
newReqs = newReqs.map((r) => parseAccount(r));
|
||||
|
||||
followRequests.set(newReqs);
|
||||
}
|
|
@ -154,8 +154,7 @@
|
|||
a.disabled,
|
||||
button.disabled {
|
||||
color: var(--text);
|
||||
opacity: .5;
|
||||
background: transparent;
|
||||
opacity: .35;
|
||||
border-color: transparent;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
import { playSound } from '$lib/sound.js';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { unread_notif_count } from '$lib/notifications.js';
|
||||
import { fetchFollowRequests, followRequests } from '$lib/followRequests.js'
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
import Logo from '$lib/../img/campfire-logo.svg';
|
||||
|
@ -24,6 +25,7 @@
|
|||
import InfoIcon from '../../img/icons/info.svg';
|
||||
import SettingsIcon from '../../img/icons/settings.svg';
|
||||
import LogoutIcon from '../../img/icons/logout.svg';
|
||||
import FollowersIcon from '../../img/icons/followers.svg';
|
||||
|
||||
const VERSION = APP_VERSION;
|
||||
const lang = Lang('en_GB');
|
||||
|
@ -40,7 +42,7 @@
|
|||
goto(`/${$server.host}/${$account.username}`);
|
||||
}
|
||||
|
||||
async function log_out() {
|
||||
async function logOut() {
|
||||
if (!confirm("This will log you out. Are you sure?")) return;
|
||||
|
||||
const res = await api.revokeToken(
|
||||
|
@ -59,6 +61,10 @@
|
|||
|
||||
goto("/");
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await fetchFollowRequests(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div id="navigation">
|
||||
|
@ -91,6 +97,19 @@
|
|||
</span>
|
||||
{/if}
|
||||
</Button>
|
||||
{#if $followRequests.length > 0}
|
||||
<Button label="Follow requests"
|
||||
href="/follow-requests"}
|
||||
active={$page.url.pathname === "/follow-requests"}>
|
||||
<svelte:fragment slot="icon">
|
||||
<FollowersIcon/>
|
||||
</svelte:fragment>
|
||||
{lang.string('navigation.follow_requests')}
|
||||
<span class="notification-count">
|
||||
{$followRequests.length}
|
||||
</span>
|
||||
</Button>
|
||||
{/if}
|
||||
<Button label="Explore" disabled>
|
||||
<svelte:fragment slot="icon">
|
||||
<ExploreIcon height="auto"/>
|
||||
|
@ -142,7 +161,7 @@
|
|||
<SettingsIcon/>
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
<Button centered label="{lang.string('navigation.log_out')}" on:click={() => log_out()}>
|
||||
<Button centered label="{lang.string('navigation.log_out')}" on:click={() => logOut()}>
|
||||
<svelte:fragment slot="icon">
|
||||
<LogoutIcon/>
|
||||
</svelte:fragment>
|
||||
|
|
|
@ -144,19 +144,22 @@
|
|||
<style>
|
||||
.notification {
|
||||
display: block;
|
||||
border-top: 1px solid color-mix(in srgb, transparent, var(--text) 50%);
|
||||
border-top: 1px solid color-mix(in srgb, transparent, var(--text) 25%);
|
||||
padding: 16px;
|
||||
text-decoration: inherit;
|
||||
color: inherit;
|
||||
transition: background-color .1s;
|
||||
cursor: pointer;
|
||||
|
||||
background-color: var(--bg-900);
|
||||
}
|
||||
.notification:first-of-type {
|
||||
border-top: none;
|
||||
|
||||
}
|
||||
|
||||
.notification:hover {
|
||||
background-color: color-mix(in srgb, var(--bg-800), transparent 50%);
|
||||
background-color: color-mix(in srgb, var(--bg-800), transparent 35%);
|
||||
}
|
||||
|
||||
header {
|
||||
|
|
36
src/lib/ui/core/PageHeader.svelte
Normal file
36
src/lib/ui/core/PageHeader.svelte
Normal file
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
export let title;
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h1>{title}</h1>
|
||||
<div class="header-items">
|
||||
<slot/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin: 16px 0;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
header .header-items {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
|
@ -7,6 +7,7 @@
|
|||
import LoginForm from '$lib/ui/LoginForm.svelte';
|
||||
import Button from '$lib/ui/Button.svelte';
|
||||
import Post from '$lib/ui/post/Post.svelte';
|
||||
import PageHeader from '../lib/ui/core/PageHeader.svelte';
|
||||
|
||||
const lang = Lang('en_GB');
|
||||
|
||||
|
@ -23,14 +24,11 @@
|
|||
</script>
|
||||
|
||||
{#if $account}
|
||||
<header>
|
||||
<h1>{lang.string('timeline.home')}</h1>
|
||||
<nav>
|
||||
<PageHeader title={lang.string('timeline.home')}>
|
||||
<Button centered active>{lang.string('timeline.home')}</Button>
|
||||
<Button centered disabled>{lang.string('timeline.local')}</Button>
|
||||
<Button centered disabled>{lang.string('timeline.federated')}</Button>
|
||||
</nav>
|
||||
</header>
|
||||
</PageHeader>
|
||||
|
||||
<div id="feed" role="feed">
|
||||
{#if $timeline.length <= 0}
|
||||
|
@ -47,30 +45,6 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin: 16px 0;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#feed {
|
||||
margin-bottom: 20vh;
|
||||
}
|
||||
|
|
144
src/routes/follow-requests/+page.svelte
Normal file
144
src/routes/follow-requests/+page.svelte
Normal file
|
@ -0,0 +1,144 @@
|
|||
<script>
|
||||
import { followRequests } from '$lib/followRequests.js';
|
||||
import PageHeader from '../../lib/ui/core/PageHeader.svelte';
|
||||
import Lang from '$lib/lang';
|
||||
import {server} from '$lib/client/server';
|
||||
import {app} from '$lib/client/app';
|
||||
import Button from '../../lib/ui/Button.svelte';
|
||||
import * as api from '$lib/api'
|
||||
|
||||
import TickIcon from '../../img/icons/tick.svg'
|
||||
import CrossIcon from '../../img/icons/cross.svg'
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const lang = Lang('en_GB');
|
||||
|
||||
async function actionRequest(account_id, approved) {
|
||||
// remove item from array first - this updates the ui and
|
||||
// makes the interaction more seamless
|
||||
$followRequests.splice(
|
||||
$followRequests.indexOf(
|
||||
$followRequests.find(r => r.id)
|
||||
),
|
||||
1
|
||||
);
|
||||
|
||||
// hack: force the state to update now that we just spliced the array
|
||||
$followRequests = $followRequests
|
||||
|
||||
if(approved) {
|
||||
await api.acceptFollowRequest(
|
||||
get(server).host,
|
||||
get(app).token,
|
||||
account_id
|
||||
)
|
||||
} else {
|
||||
await api.rejectFollowRequest(
|
||||
get(server).host,
|
||||
get(app).token,
|
||||
account_id
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// aliases
|
||||
const acceptRequest = (id) => actionRequest(id, true);
|
||||
const denyRequest = (id) => actionRequest(id, false);
|
||||
</script>
|
||||
|
||||
<PageHeader title={lang.string('navigation.follow_requests')}/>
|
||||
|
||||
{#if $followRequests.length < 1}
|
||||
<p class="request-zero">{lang.string('follow_requests.none')}</p>
|
||||
{/if}
|
||||
|
||||
{#each $followRequests as req}
|
||||
<div class="request">
|
||||
<a href="/{$server.host}/@{req.fqn}" class="request-avatar-container" on:mouseup|stopPropagation>
|
||||
<img src={req.avatar_url} alt="" width="48" height="48" class="post-avatar" loading="lazy" decoding="async">
|
||||
</a>
|
||||
<div class="info">
|
||||
<div class="request-user-info">
|
||||
<a href="/{$server.host}/@{req.fqn}" class="name">{@html req.rich_name}</a>
|
||||
<span class="username">{req.mention}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="request-options">
|
||||
<Button filled title="Yes" on:click={() => acceptRequest(req.id)}>
|
||||
<TickIcon width="24px"/>
|
||||
</Button>
|
||||
<Button title="No" on:click={() => denyRequest(req.id)}>
|
||||
<CrossIcon width="24px"/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style>
|
||||
.request {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
background: var(--bg-900);
|
||||
padding: .5rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.request a,
|
||||
.request a:visited {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.request a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.request-avatar-container {
|
||||
margin-right: 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.post-avatar {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.request-user-info {
|
||||
margin-top: -2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.request-user-info a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.request-user-info .username {
|
||||
opacity: .8;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.request-options {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.request-options :global(button) {
|
||||
width: fit-content;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.request-zero {
|
||||
opacity: 0.8;
|
||||
font-size: 0.95rem;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -4,6 +4,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Notification from '$lib/ui/Notification.svelte';
|
||||
import PageHeader from '../../lib/ui/core/PageHeader.svelte';
|
||||
import Lang from '$lib/lang';
|
||||
|
||||
const lang = Lang('en_GB');
|
||||
|
@ -33,9 +34,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<h1>{lang.string('navigation.notifications')}</h1>
|
||||
</header>
|
||||
<PageHeader title={lang.string('navigation.notifications')}/>
|
||||
|
||||
<div class="notifications">
|
||||
{#if $notifications.length === 0}
|
||||
|
@ -50,20 +49,6 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
margin: 16px 0 8px 0;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.loading {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue