Compare commits
10 Commits
1784c849fa
...
d049f3c82f
Author | SHA1 | Date |
---|---|---|
Natty | d049f3c82f | |
Natty | a0bdab23e7 | |
Natty | b4e01fee63 | |
Natty | 36c3507d84 | |
Natty | 2525b3a50a | |
Natty | 3837980e50 | |
Natty | 5c8db9b243 | |
Natty | 98c5bcb4c4 | |
Natty | df275de905 | |
Natty | dde74dd56b |
|
@ -1603,6 +1603,14 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mmm_parser"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
|
@ -14,6 +14,7 @@ members = [
|
|||
"fe_calckey",
|
||||
"magnetar_common",
|
||||
"magnetar_sdk",
|
||||
"magnetar_mmm_parser",
|
||||
"core"
|
||||
]
|
||||
|
||||
|
@ -36,6 +37,7 @@ hyper = "0.14"
|
|||
js-sys = "0.3"
|
||||
log = "0.4"
|
||||
miette = "5.9"
|
||||
nom = "7"
|
||||
percent-encoding = "2.2"
|
||||
redis = "0.23"
|
||||
reqwest = "0.11"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<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="twitter:card" content="summary">
|
||||
<meta name="og:site_name" content="{{ instance_name }}">
|
||||
<meta property="og:site_name" content="{{ instance_name }}">
|
||||
<title>{{ title | capitalize }}</title>
|
||||
<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 }}">
|
||||
|
@ -29,10 +29,10 @@
|
|||
<meta name="robots" content="noindex">
|
||||
{% endif %}
|
||||
|
||||
<meta name="og:title" content="{{ title | capitalize }}">
|
||||
<meta name="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 name="og:image:alt" content="{% if image_alt %}{{ image_alt }}{% else %}The instance's logo{% endif %}">
|
||||
<meta property="og:title" content="{{ title | capitalize }}">
|
||||
<meta property="og:description" content="{% if description %}{{ description }}{% else %}Magnetar, an open source social media platform{% endif %}">
|
||||
<meta property="og:image" content="{% if image %}{{ image }}{% else %}/static-assets/favicon.png{% endif %}">
|
||||
<meta property="og:image:alt" content="{% if image_alt %}{{ image_alt }}{% else %}The instance's logo{% endif %}">
|
||||
|
||||
<style>
|
||||
{{ style_css | safe }}
|
||||
|
|
|
@ -163,6 +163,7 @@ export type Note = {
|
|||
url?: string;
|
||||
updatedAt?: DateString;
|
||||
isHidden?: boolean;
|
||||
hasRenotedBefore?: boolean;
|
||||
};
|
||||
|
||||
export type NoteReaction = {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<button ref="thumbnail" class="zdjebgpv">
|
||||
<button
|
||||
ref="thumbnail"
|
||||
class="zdjebgpv"
|
||||
:class="{ 'no-alt': !file.comment }"
|
||||
>
|
||||
<ImgWithBlurhash
|
||||
v-if="isThumbnailAvailable"
|
||||
:hash="file.blurhash"
|
||||
|
@ -92,6 +96,10 @@ const isThumbnailAvailable = computed(() => {
|
|||
padding: 0;
|
||||
cursor: pointer;
|
||||
|
||||
&.no-alt {
|
||||
border: 2px solid orangered;
|
||||
}
|
||||
|
||||
> .icon-sub {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
|
|
|
@ -27,19 +27,38 @@
|
|||
/>
|
||||
<div v-if="image.type === 'image/gif'" class="gif">GIF</div>
|
||||
</a>
|
||||
<button
|
||||
v-tooltip="i18n.ts.hide"
|
||||
class="_button hide"
|
||||
@click="hide = true"
|
||||
>
|
||||
<i class="ph-eye-slash ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<div class="_image_controls">
|
||||
<button
|
||||
v-if="!image.comment"
|
||||
v-tooltip="i18n.ts.noAltText"
|
||||
class="_button _image_control_btn _image_control_warn"
|
||||
@click="noAltTextPopup"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from "vue";
|
||||
import type * as misskey from "calckey-js";
|
||||
import * as os from "@/os";
|
||||
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
|
||||
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
|
||||
import { defaultStore } from "@/store";
|
||||
|
@ -52,6 +71,26 @@ const props = defineProps<{
|
|||
|
||||
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 =
|
||||
props.raw || defaultStore.state.loadRawImages
|
||||
? props.image.url
|
||||
|
@ -110,22 +149,34 @@ watch(
|
|||
position: relative;
|
||||
background: var(--bg);
|
||||
|
||||
> .hide {
|
||||
display: block;
|
||||
> ._image_controls {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
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;
|
||||
gap: 6px;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
|
||||
> i {
|
||||
> ._image_control_btn {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,13 @@
|
|||
</span>
|
||||
<span v-if="!isLocal">
|
||||
<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 v-if="isVoted"> · {{ i18n.ts._poll.voted }}</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";
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
note: misskey.entities.Note & Required<Pick<misskey.entities.Note, "poll">>;
|
||||
readOnly?: boolean;
|
||||
}>();
|
||||
|
||||
const pollRefreshing = ref(false);
|
||||
const remaining = ref(-1);
|
||||
|
||||
const total = computed(() => sum(props.note.poll.choices.map((x) => x.votes)));
|
||||
|
@ -100,7 +107,7 @@ if (props.note.poll.expiresAt) {
|
|||
const tick = () => {
|
||||
remaining.value = Math.floor(
|
||||
Math.max(
|
||||
new Date(props.note.poll.expiresAt).getTime() - Date.now(),
|
||||
new Date(props.note.poll.expiresAt!).getTime() - Date.now(),
|
||||
0
|
||||
) / 1000
|
||||
);
|
||||
|
@ -117,13 +124,27 @@ if (props.note.poll.expiresAt) {
|
|||
|
||||
async function refresh() {
|
||||
if (!props.note.uri) return;
|
||||
const obj = await os.apiWithDialog("ap/show", { uri: props.note.uri });
|
||||
if (obj.type === "Note" && obj.object.poll) {
|
||||
props.note.poll = obj.object.poll;
|
||||
}
|
||||
|
||||
pollRefreshing.value = true;
|
||||
|
||||
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();
|
||||
|
||||
if (props.readOnly || closed.value || isVoted.value) return;
|
||||
|
|
|
@ -7,7 +7,13 @@
|
|||
:class="{ renoted: hasRenotedBefore }"
|
||||
@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>
|
||||
</button>
|
||||
<button v-else class="eddddedb _button">
|
||||
|
@ -36,6 +42,8 @@ const props = defineProps<{
|
|||
|
||||
const buttonRef = ref<HTMLElement>();
|
||||
|
||||
const hasRenotedBefore = ref(props.note.hasRenotedBefore ?? false);
|
||||
|
||||
const canRenote = computed(
|
||||
() =>
|
||||
["public", "home"].includes(props.note.visibility) ||
|
||||
|
@ -68,14 +76,6 @@ useTooltip(buttonRef, async (showing) => {
|
|||
const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
||||
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> = [];
|
||||
|
||||
if (props.note.visibility === "public") {
|
||||
|
@ -88,7 +88,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
renoteId: props.note.id,
|
||||
visibility: "public",
|
||||
});
|
||||
hasRenotedBefore = true;
|
||||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
|
@ -115,7 +115,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
renoteId: props.note.id,
|
||||
visibility: "home",
|
||||
});
|
||||
hasRenotedBefore = true;
|
||||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
|
@ -143,7 +143,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
visibility: "specified",
|
||||
visibleUserIds: props.note.visibleUserIds,
|
||||
});
|
||||
hasRenotedBefore = true;
|
||||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
|
@ -168,7 +168,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
renoteId: props.note.id,
|
||||
visibility: "followers",
|
||||
});
|
||||
hasRenotedBefore = true;
|
||||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
|
@ -206,7 +206,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
localOnly: true,
|
||||
}
|
||||
);
|
||||
hasRenotedBefore = true;
|
||||
hasRenotedBefore.value = true;
|
||||
const el =
|
||||
ev &&
|
||||
((ev.currentTarget ?? ev.target) as
|
||||
|
@ -236,7 +236,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
});
|
||||
}
|
||||
|
||||
if (hasRenotedBefore) {
|
||||
if (hasRenotedBefore.value) {
|
||||
buttonActions.push({
|
||||
text: i18n.ts.unrenote,
|
||||
icon: "ph-trash ph-bold ph-lg",
|
||||
|
@ -245,7 +245,7 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
|||
os.api("notes/unrenote", {
|
||||
noteId: props.note.id,
|
||||
});
|
||||
hasRenotedBefore = false;
|
||||
hasRenotedBefore.value = false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -15,6 +15,6 @@ export const lang = localStorage.getItem("lang");
|
|||
export const langs = _LANGS_;
|
||||
export const locale = JSON.parse(localStorage.getItem("locale"));
|
||||
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 debug = localStorage.getItem("debug") === "true";
|
||||
|
|
|
@ -145,11 +145,10 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineComponent, inject, watch } from "vue";
|
||||
import { computed, watch } from "vue";
|
||||
import MkButton from "@/components/MkButton.vue";
|
||||
import * as os from "@/os";
|
||||
import MkContainer from "@/components/MkContainer.vue";
|
||||
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import MkGalleryPostPreview from "@/components/MkGalleryPostPreview.vue";
|
||||
import MkFollowButton from "@/components/MkFollowButton.vue";
|
||||
|
|
|
@ -599,6 +599,8 @@ troubleshooting: "Poradce při potížích"
|
|||
whatIsNew: "Zobrazit změny"
|
||||
translate: "Přeložit"
|
||||
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"
|
||||
tablet: "Tablet"
|
||||
auto: "Auto"
|
||||
|
|
|
@ -942,6 +942,9 @@ deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
|||
incorrectPassword: "Incorrect password."
|
||||
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
||||
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"
|
||||
leaveGroupConfirm: "Are you sure you want to leave \"{name}\"?"
|
||||
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
|
||||
|
|
|
@ -21,8 +21,10 @@ use std::sync::Arc;
|
|||
use std::time::Duration;
|
||||
use tera::Tera;
|
||||
use thiserror::Error;
|
||||
use tokio::signal;
|
||||
use tower_http::services::ServeFile;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::error;
|
||||
use tracing::log::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
|
@ -117,6 +119,33 @@ async fn main() -> miette::Result<()> {
|
|||
info!("Serving on: {addr}");
|
||||
axum::Server::bind(&addr)
|
||||
.serve(app.into_make_service())
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await
|
||||
.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...");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "mmm_parser"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
nom = { workspace = true }
|
|
@ -0,0 +1,5 @@
|
|||
# MMM
|
||||
|
||||
Magnetar {marinated, modified} Markdown?
|
||||
|
||||
#TODO: Finish docs
|
Loading…
Reference in New Issue