Compare commits

..

10 Commits

Author SHA1 Message Date
Natty d049f3c82f
Created a project for the MMM parser 2023-10-01 10:46:26 +02:00
Natty a0bdab23e7
Frontend: Fixed document titles
ci/woodpecker/push/ociImagePush Pipeline was successful Details
2023-10-01 01:25:20 +02:00
Natty b4e01fee63
Frontend: Alt text indicators 2023-10-01 01:05:18 +02:00
Natty 36c3507d84
Frontend: Replaced the refreshing text with a spinner
ci/woodpecker/push/ociImagePush Pipeline was successful Details
2023-09-30 23:03:48 +02:00
Natty 2525b3a50a
Frontend: Graceful shutdown of backend 2023-09-30 23:02:39 +02:00
Natty 3837980e50
Frontend: Fixed imports (???)
ci/woodpecker/push/ociImagePush Pipeline was successful Details
2023-09-30 22:40:58 +02:00
Natty 5c8db9b243
Frontend: Cleaned up poll code
ci/woodpecker/push/ociImagePush Pipeline was successful Details
2023-09-30 22:16:27 +02:00
Natty 98c5bcb4c4
Frontend: Different icon for already-renoted buttons
ci/woodpecker/push/ociImagePush Pipeline was successful Details
2023-09-30 21:31:00 +02:00
Natty df275de905
Frontend: Fixed a wrong renote cancel condition 2023-09-30 21:16:56 +02:00
Natty dde74dd56b
Frontend: Fixed renote button
ci/woodpecker/push/ociImagePush Pipeline was successful Details
2023-09-30 21:03:16 +02:00
11 changed files with 165 additions and 51 deletions

View File

@ -8,7 +8,7 @@
<meta name="theme-color" content="{% if theme_color %} {{ theme_color }} {% else %} #31748f {% endif %}"> <meta name="theme-color" content="{% if theme_color %} {{ theme_color }} {% else %} #31748f {% endif %}">
<meta name="theme-color-orig" content="{% if theme_color %} {{ theme_color }} {% else %} #31748f {% endif %}"> <meta name="theme-color-orig" content="{% if theme_color %} {{ theme_color }} {% else %} #31748f {% endif %}">
<meta name="twitter:card" content="summary"> <meta name="twitter:card" content="summary">
<meta name="og:site_name" content="{{ instance_name }}"> <meta property="og:site_name" content="{{ instance_name }}">
<title>{{ title | capitalize }}</title> <title>{{ title | capitalize }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="/favicon.ico?{{ timestamp }}"> <link rel="icon" href="/favicon.ico?{{ timestamp }}">
@ -29,10 +29,10 @@
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
{% endif %} {% endif %}
<meta name="og:title" content="{{ title | capitalize }}"> <meta property="og:title" content="{{ title | capitalize }}">
<meta name="og:description" content="{% if description %}{{ description }}{% else %}Magnetar, an open source social media platform{% endif %}"> <meta property="og:description" content="{% if description %}{{ description }}{% else %}Magnetar, an open source social media platform{% endif %}">
<meta name="og:image" content="{% if image %}{{ image }}{% else %}/static-assets/favicon.png{% endif %}"> <meta property="og:image" content="{% if image %}{{ image }}{% else %}/static-assets/favicon.png{% endif %}">
<meta name="og:image:alt" content="{% if image_alt %}{{ image_alt }}{% else %}The instance's logo{% endif %}"> <meta property="og:image:alt" content="{% if image_alt %}{{ image_alt }}{% else %}The instance's logo{% endif %}">
<style> <style>
{{ style_css | safe }} {{ style_css | safe }}

View File

@ -163,6 +163,7 @@ export type Note = {
url?: string; url?: string;
updatedAt?: DateString; updatedAt?: DateString;
isHidden?: boolean; isHidden?: boolean;
hasRenotedBefore?: boolean;
}; };
export type NoteReaction = { export type NoteReaction = {

View File

@ -1,5 +1,9 @@
<template> <template>
<button ref="thumbnail" class="zdjebgpv"> <button
ref="thumbnail"
class="zdjebgpv"
:class="{ 'no-alt': !file.comment }"
>
<ImgWithBlurhash <ImgWithBlurhash
v-if="isThumbnailAvailable" v-if="isThumbnailAvailable"
:hash="file.blurhash" :hash="file.blurhash"
@ -92,6 +96,10 @@ const isThumbnailAvailable = computed(() => {
padding: 0; padding: 0;
cursor: pointer; cursor: pointer;
&.no-alt {
border: 2px solid orangered;
}
> .icon-sub { > .icon-sub {
position: absolute; position: absolute;
width: 30%; width: 30%;

View File

@ -27,19 +27,38 @@
/> />
<div v-if="image.type === 'image/gif'" class="gif">GIF</div> <div v-if="image.type === 'image/gif'" class="gif">GIF</div>
</a> </a>
<button <div class="_image_controls">
v-tooltip="i18n.ts.hide" <button
class="_button hide" v-if="!image.comment"
@click="hide = true" v-tooltip="i18n.ts.noAltText"
> class="_button _image_control_btn _image_control_warn"
<i class="ph-eye-slash ph-bold ph-lg"></i> @click="noAltTextPopup"
</button> >
<i class="ph-file-x ph-bold ph-lg"></i>
</button>
<button
v-if="image.comment"
v-tooltip="i18n.ts.altText"
class="_button _image_control_btn"
@click="altTextPopup"
>
<i class="ph-file-text ph-bold ph-lg"></i>
</button>
<button
v-tooltip="i18n.ts.hide"
class="_button _image_control_btn"
@click="hide = true"
>
<i class="ph-eye-slash ph-bold ph-lg"></i>
</button>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch } from "vue"; import { watch } from "vue";
import type * as misskey from "calckey-js"; import type * as misskey from "calckey-js";
import * as os from "@/os";
import { getStaticImageUrl } from "@/scripts/get-static-image-url"; import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue"; import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
@ -52,6 +71,26 @@ const props = defineProps<{
let hide = $ref(true); let hide = $ref(true);
function noAltTextPopup(event: MouseEvent) {
os.alert({
type: "warning",
title: i18n.ts.noAltText,
text: i18n.ts.noAltTextDescription,
});
event.stopPropagation();
}
function altTextPopup(event: MouseEvent) {
os.alert({
type: "info",
title: i18n.ts.altText,
text: props.image.comment,
});
event.stopPropagation();
}
const url = const url =
props.raw || defaultStore.state.loadRawImages props.raw || defaultStore.state.loadRawImages
? props.image.url ? props.image.url
@ -110,22 +149,34 @@ watch(
position: relative; position: relative;
background: var(--bg); background: var(--bg);
> .hide { > ._image_controls {
display: block; display: flex;
position: absolute; position: absolute;
border-radius: 6px; gap: 6px;
background-color: var(--accentedBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
top: 12px; top: 12px;
right: 12px; right: 12px;
> i { > ._image_control_btn {
display: block; display: block;
border-radius: 6px;
background-color: var(--accentedBg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
border: 2px solid transparent;
> i {
display: block;
}
}
> ._image_control_warn {
color: #f6b195;
border: 2px solid #fa895c;
box-sizing: border-box;
} }
} }

View File

@ -42,7 +42,13 @@
</span> </span>
<span v-if="!isLocal"> <span v-if="!isLocal">
<span> · </span> <span> · </span>
<a @click.stop="refresh">{{ i18n.ts.reload }}</a> <a v-if="!pollRefreshing" @click.stop="refresh">{{
i18n.ts.reload
}}</a>
<i
v-else
class="ph-circle-notch ph-bold ph-lg fa-pulse ph-fw ph-lg"
></i>
</span> </span>
<span v-if="isVoted"> · {{ i18n.ts._poll.voted }}</span> <span v-if="isVoted"> · {{ i18n.ts._poll.voted }}</span>
<span v-else-if="closed"> · {{ i18n.ts._poll.closed }}</span> <span v-else-if="closed"> · {{ i18n.ts._poll.closed }}</span>
@ -61,10 +67,11 @@ import { i18n } from "@/i18n";
import { useInterval } from "@/scripts/use-interval"; import { useInterval } from "@/scripts/use-interval";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note & Required<Pick<misskey.entities.Note, "poll">>;
readOnly?: boolean; readOnly?: boolean;
}>(); }>();
const pollRefreshing = ref(false);
const remaining = ref(-1); const remaining = ref(-1);
const total = computed(() => sum(props.note.poll.choices.map((x) => x.votes))); const total = computed(() => sum(props.note.poll.choices.map((x) => x.votes)));
@ -100,7 +107,7 @@ if (props.note.poll.expiresAt) {
const tick = () => { const tick = () => {
remaining.value = Math.floor( remaining.value = Math.floor(
Math.max( Math.max(
new Date(props.note.poll.expiresAt).getTime() - Date.now(), new Date(props.note.poll.expiresAt!).getTime() - Date.now(),
0 0
) / 1000 ) / 1000
); );
@ -117,13 +124,27 @@ if (props.note.poll.expiresAt) {
async function refresh() { async function refresh() {
if (!props.note.uri) return; if (!props.note.uri) return;
const obj = await os.apiWithDialog("ap/show", { uri: props.note.uri });
if (obj.type === "Note" && obj.object.poll) { pollRefreshing.value = true;
props.note.poll = obj.object.poll;
} os.api("ap/show", { uri: props.note.uri })
.then((obj) => {
if (obj && obj.type === "Note" && obj.object.poll) {
props.note.poll = obj.object.poll;
}
})
.catch((err) => {
os.alert({
type: "error",
text: err.message + "\n" + (err as any).id,
});
})
.finally(() => {
pollRefreshing.value = false;
});
} }
const vote = async (id) => { const vote = async (id: number) => {
pleaseLogin(); pleaseLogin();
if (props.readOnly || closed.value || isVoted.value) return; if (props.readOnly || closed.value || isVoted.value) return;

View File

@ -7,7 +7,13 @@
:class="{ renoted: hasRenotedBefore }" :class="{ renoted: hasRenotedBefore }"
@click="renote(false, $event)" @click="renote(false, $event)"
> >
<i class="ph-repeat ph-bold ph-lg"></i> <i
class="ph-bold ph-lg"
:class="{
'ph-repeat': !hasRenotedBefore,
'ph-repeat-once': hasRenotedBefore,
}"
></i>
<p v-if="count > 0 && !detailedView" class="count">{{ count }}</p> <p v-if="count > 0 && !detailedView" class="count">{{ count }}</p>
</button> </button>
<button v-else class="eddddedb _button"> <button v-else class="eddddedb _button">
@ -36,6 +42,8 @@ const props = defineProps<{
const buttonRef = ref<HTMLElement>(); const buttonRef = ref<HTMLElement>();
const hasRenotedBefore = ref(props.note.hasRenotedBefore ?? false);
const canRenote = computed( const canRenote = computed(
() => () =>
["public", "home"].includes(props.note.visibility) || ["public", "home"].includes(props.note.visibility) ||
@ -68,14 +76,6 @@ useTooltip(buttonRef, async (showing) => {
const renote = async (viaKeyboard = false, ev?: MouseEvent) => { const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
pleaseLogin(); pleaseLogin();
const renotes = await os.api("notes/renotes", {
noteId: props.note.id,
userId: $i.id,
limit: 1,
});
const hasRenotedBefore = renotes.length > 0;
let buttonActions: Array<MenuItem> = []; let buttonActions: Array<MenuItem> = [];
if (props.note.visibility === "public") { if (props.note.visibility === "public") {
@ -88,7 +88,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
renoteId: props.note.id, renoteId: props.note.id,
visibility: "public", visibility: "public",
}); });
hasRenotedBefore = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as
@ -115,7 +115,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
renoteId: props.note.id, renoteId: props.note.id,
visibility: "home", visibility: "home",
}); });
hasRenotedBefore = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as
@ -143,7 +143,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
visibility: "specified", visibility: "specified",
visibleUserIds: props.note.visibleUserIds, visibleUserIds: props.note.visibleUserIds,
}); });
hasRenotedBefore = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as
@ -168,7 +168,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
renoteId: props.note.id, renoteId: props.note.id,
visibility: "followers", visibility: "followers",
}); });
hasRenotedBefore = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as
@ -206,7 +206,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
localOnly: true, localOnly: true,
} }
); );
hasRenotedBefore = true; hasRenotedBefore.value = true;
const el = const el =
ev && ev &&
((ev.currentTarget ?? ev.target) as ((ev.currentTarget ?? ev.target) as
@ -236,7 +236,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
}); });
} }
if (hasRenotedBefore) { if (hasRenotedBefore.value) {
buttonActions.push({ buttonActions.push({
text: i18n.ts.unrenote, text: i18n.ts.unrenote,
icon: "ph-trash ph-bold ph-lg", icon: "ph-trash ph-bold ph-lg",
@ -245,7 +245,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
os.api("notes/unrenote", { os.api("notes/unrenote", {
noteId: props.note.id, noteId: props.note.id,
}); });
hasRenotedBefore = false; hasRenotedBefore.value = false;
}, },
}); });
} }

View File

@ -15,6 +15,6 @@ export const lang = localStorage.getItem("lang");
export const langs = _LANGS_; export const langs = _LANGS_;
export const locale = JSON.parse(localStorage.getItem("locale")); export const locale = JSON.parse(localStorage.getItem("locale"));
export const version = _VERSION_; export const version = _VERSION_;
export const instanceName = siteName === "Calckey" ? host : siteName; export const instanceName = siteName === "Magnetar" ? host : siteName;
export const ui = localStorage.getItem("ui"); export const ui = localStorage.getItem("ui");
export const debug = localStorage.getItem("debug") === "true"; export const debug = localStorage.getItem("debug") === "true";

View File

@ -145,11 +145,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineComponent, inject, watch } from "vue"; import { computed, watch } from "vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import * as os from "@/os"; import * as os from "@/os";
import MkContainer from "@/components/MkContainer.vue"; import MkContainer from "@/components/MkContainer.vue";
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
import MkPagination from "@/components/MkPagination.vue"; import MkPagination from "@/components/MkPagination.vue";
import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue"; import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue";
import MkFollowButton from "@/components/MkFollowButton.vue"; import MkFollowButton from "@/components/MkFollowButton.vue";

View File

@ -599,6 +599,8 @@ troubleshooting: "Poradce při potížích"
whatIsNew: "Zobrazit změny" whatIsNew: "Zobrazit změny"
translate: "Přeložit" translate: "Přeložit"
hide: "Skrýt" hide: "Skrýt"
noAltText: "Chybějící popis obrázku"
noAltTextDescription: "Tento obrázek nemá alt text. Popis obrázku je užitečný pro zrakově-postižené uživatele a poskytuje lepší kontext pro všechny."
smartphone: "Telefon" smartphone: "Telefon"
tablet: "Tablet" tablet: "Tablet"
auto: "Auto" auto: "Auto"

View File

@ -942,6 +942,9 @@ deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
incorrectPassword: "Incorrect password." incorrectPassword: "Incorrect password."
voteConfirm: "Confirm your vote for \"{choice}\"?" voteConfirm: "Confirm your vote for \"{choice}\"?"
hide: "Hide" hide: "Hide"
altText: "Alt text"
noAltText: "No alt text"
noAltTextDescription: "This image does not have alt text. Alt text is useful for vision impaired users, as well as for everyone else for context."
leaveGroup: "Leave group" leaveGroup: "Leave group"
leaveGroupConfirm: "Are you sure you want to leave \"{name}\"?" leaveGroupConfirm: "Are you sure you want to leave \"{name}\"?"
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile" useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"

View File

@ -21,8 +21,10 @@ use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tera::Tera; use tera::Tera;
use thiserror::Error; use thiserror::Error;
use tokio::signal;
use tower_http::services::ServeFile; use tower_http::services::ServeFile;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::error;
use tracing::log::info; use tracing::log::info;
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
@ -117,6 +119,33 @@ async fn main() -> miette::Result<()> {
info!("Serving on: {addr}"); info!("Serving on: {addr}");
axum::Server::bind(&addr) axum::Server::bind(&addr)
.serve(app.into_make_service()) .serve(app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await .await
.map_err(|e| miette!("Error running server: {}", e)) .map_err(|e| miette!("Error running server: {}", e))
} }
async fn shutdown_signal() {
let ctrl_c = async {
if let Err(e) = signal::ctrl_c().await {
error!("Ctrl+C signal handler error: {}", e);
}
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("SIGTERM handler error")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
info!("Shutting down...");
}