Merge remote-tracking branch 'origin/main' into fully-neutral-ru-p

This commit is contained in:
Valentyne Stigloher 2025-04-17 00:09:46 +02:00
commit 8290ddbfc9
73 changed files with 1188 additions and 862 deletions

1
.gitignore vendored
View File

@ -15,6 +15,7 @@
/keys
/locale/*.schema.json
/locale/fonts.ts
/cache
/calendar

View File

@ -13,7 +13,7 @@ check:
needs: []
rules:
- if: $CI_COMMIT_BRANCH
image: node:20.12.2
image: node:22.14.0
before_script:
- set -o pipefail
- export NODE_ENV=development
@ -100,7 +100,7 @@ build:
tags: ['build']
rules:
- if: $CI_COMMIT_REF_PROTECTED == 'true'
image: node:20.12.2
image: node:22.14.0
script:
# see https://docs.gitlab.com/ee/ci/jobs/ssh_keys.html#ssh-keys-when-using-the-docker-executor
- 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'
@ -145,7 +145,7 @@ build:
-
job: 'build'
artifacts: false
image: node:20.12.2
image: node:22.14.0
before_script:
# see https://docs.gitlab.com/ee/ci/jobs/ssh_keys.html#ssh-keys-when-using-the-docker-executor
- 'command -v ssh-agent >/dev/null || ( apt-get update -y && apt-get install openssh-client -y )'

2
.nvmrc
View File

@ -1 +1 @@
v20.12.2
22.14.0

View File

@ -24,12 +24,6 @@ test:
run:
pnpm dev
start:
node_modules/.bin/avris-daemonise start webserver pnpm dev
stop:
node_modules/.bin/avris-daemonise stop webserver
build: install
pnpm build
pnpm run-file server/sentry.ts release
@ -37,6 +31,7 @@ build: install
deploy:
pnpm install -P
pnpm run-file server/migrate.ts
-redis-cli KEYS \"${NUXT_PUBLIC_ENV}:cache:*\" | xargs -n 100 redis-cli DEL
-pnpm run-file server/sentry.ts deploy
migrate:

View File

@ -97,7 +97,7 @@ To work effectively with this project, it is recommended to configure these poin
- Integrating Vitest to easily view test results
- Use `yaml` syntax highlighting for `.suml` files
- Link `locale/config.schema.json` to `locale/*/config.suml` for schema validation and type hints
(the schema file is automatically generated by `locale/generateSchemas.ts` in `make install`)
(the schema file is automatically generated by `locale/generate.ts` in `make install`)
### Git

View File

@ -5,6 +5,7 @@ import { useHead, useSeoMeta } from '#imports';
import useConfig from '~/composables/useConfig.ts';
import { getDefaultSeo } from '~/composables/useSimpleHead.ts';
import { getUrlForLocale } from '~/src/domain.ts';
import { formatFonts } from '~/src/fonts.ts';
const { $translator: translator } = useNuxtApp();
const config = useConfig();
@ -20,8 +21,8 @@ useHead({
style: [{
innerHTML:
`:root {
--font-headings: ${config.style.fontHeadings.map((font) => `'${font}'`).join(',')};
--font-text: ${config.style.fontText.map((font) => `'${font}'`).join(',')};
--font-headings: ${formatFonts(config.style.fontHeadings)};
--font-text: ${formatFonts(config.style.fontText)};
}`,
}],
link: [

View File

@ -180,7 +180,7 @@ body[data-theme="dark"] {
.bg-light, .text-bg-light { background-color: #111 !important; }
.bg-dark, .text-bg-dark { background-color: #f8f9fa !important; }
.text-bg-light { color: #fff !important; }
.text-bg-dark { color: #000 !important; }
.text-bg-dark { color: #000 !important; }
.bg-white { background-color: #000 !important; }
.bg-white.text-white,
.bg-dark.text-white,

View File

@ -313,9 +313,9 @@ form[inert] {
}
}
.list-group-item-active {
color: $primary;
border-inline-start: 3px solid $primary;
padding-inline-start: calc(#{$list-group-item-padding-x} - 2px);
background-color: $list-group-hover-bg !important;
border-inline-start: 3px solid $primary !important;
padding-inline-start: calc(#{$list-group-item-padding-x} - 2px) !important;
}
.list-group-item-bg {
@ -409,4 +409,4 @@ textarea.form-control,
textarea.form-control-sm {
field-sizing: content;
min-height: 5rem;
}
}

View File

@ -21,7 +21,7 @@
<template #row="s">
<template v-if="s && s.el.susUsername">
<div>
<a :href="`${getLocaleForUrl('_')}/@${s.el.susUsername}`" target="_blank" rel="noopener">
<a :href="`${getUrlForLocale('_')}/@${s.el.susUsername}`" target="_blank" rel="noopener">
@{{ s.el.susUsername }}
</a>
<ul v-if="s.el.profiles" class="small list-inline">
@ -36,7 +36,7 @@
<span v-if="s.el.isAutomatic" class="badge bg-info">
Keyword found
</span>
<a v-else :href="`${getLocaleForUrl('_')}/@${s.el.reporterUsername}`" target="_blank" rel="noopener">
<a v-else :href="`${getUrlForLocale('_')}/@${s.el.reporterUsername}`" target="_blank" rel="noopener">
@{{ s.el.reporterUsername }}
</a>
<small>({{ $datetime($ulidTime(s.el.id)) }})</small>
@ -86,7 +86,7 @@
import useConfig from '../composables/useConfig.ts';
import useDialogue from '../composables/useDialogue.ts';
import { getLocaleForUrl } from '~/src/domain.ts';
import { getUrlForLocale } from '~/src/domain.ts';
export default {
props: {
@ -101,7 +101,7 @@ export default {
};
},
methods: {
getLocaleForUrl,
getUrlForLocale,
formatComment(comment) {
return comment
.split(', ')

View File

@ -283,7 +283,7 @@ const addBrackets = (str: string): string => {
</div>
<TabsNav
:tabs="['general', 'cards', 'socials', 'circles', 'backup']"
:tabs="['general', 'cards', 'socials', 'circles', 'blocks', 'backup']"
pills
showheaders
navclass="mb-3 border-bottom-0"
@ -562,6 +562,14 @@ const addBrackets = (str: string): string => {
</nuxt-link>
</template>
<template #blocks-header>
<Icon v="ban" />
<T>profile.blocks.header</T>
</template>
<template #blocks>
<BlocksList />
</template>
<template #backup-header>
<Icon v="copy" />
<T>profile.backup.headerShort</T>

View File

@ -1,6 +1,6 @@
<template>
<div v-if="$user() && $user().username !== user.username">
<section v-if="$user()">
<section>
<a v-if="!showReportForm" href="#" class="small" @click.prevent="showReportForm = true">
<Icon v="spider" />
<T>report.action</T>
@ -37,6 +37,12 @@
<T>report.sent</T>
</div>
</section>
<section>
<a href="#" class="small" @click.prevent="block">
<Icon v="ban" />
<T>profile.blocks.action</T>
</a>
</section>
<section v-if="$isGranted('users') || $isGranted('community')">
<div v-if="banSnapshot" class="my-3">
<a
@ -77,7 +83,7 @@
<tr v-for="proposal in banProposals">
<td>{{ $datetime($ulidTime(proposal.id)) }}</td>
<td>
<a :href="`${getLocaleForUrl('_')}/@${proposal.bannedByUsername}`" target="_blank" rel="noopener">
<a :href="`${getUrlForLocale('_')}/@${proposal.bannedByUsername}`" target="_blank" rel="noopener">
@{{ proposal.bannedByUsername }}
</a>
</td>
@ -156,7 +162,7 @@
<tr v-for="message in messages">
<td>{{ $datetime($ulidTime(message.id)) }}</td>
<td>
<a :href="`${getLocaleForUrl('_')}/@${message.adminUsername}`" target="_blank" rel="noopener">
<a :href="`${getUrlForLocale('_')}/@${message.adminUsername}`" target="_blank" rel="noopener">
@{{ message.adminUsername }}
</a>
</td>
@ -223,7 +229,7 @@ import useDialogue from '../composables/useDialogue.ts';
import forbidden from '../src/forbidden.ts';
import { sleep } from '../src/helpers.ts';
import { getLocaleForUrl } from '~/src/domain.ts';
import { getUrlForLocale } from '~/src/domain.ts';
export default {
props: {
@ -341,7 +347,7 @@ Unfortunately, I need to remove your account.
}
},
methods: {
getLocaleForUrl,
getUrlForLocale,
async ban() {
await this.dialogue.confirm(this.$t('ban.confirm', { username: this.user.username }), 'danger');
this.saving = true;
@ -380,6 +386,11 @@ Unfortunately, I need to remove your account.
this.saving = false;
}
},
async block() {
await this.dialogue.confirm(this.$t('profile.blocks.confirm', { username: this.user.username }), 'danger');
await this.dialogue.postWithAlertOnError(`/api/user/block/${this.user.id}`);
window.location.reload();
},
copyProposal(proposal) {
this.user.bannedReason = proposal.bannedReason;
this.user.bannedTerms = proposal.bannedTerms.split(',');

42
components/BlocksList.vue Normal file
View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import { useFetch } from 'nuxt/app';
import useDialogue from '~/composables/useDialogue.ts';
const { $translator: translator } = useNuxtApp();
const dialogue = useDialogue();
type Block = {
id: string;
to_userId: string;
to_username: string;
};
const blocks = useFetch<Block[]>(`/api/user/blocks`, { lazy: true });
const unblock = async (block: Block) => {
await dialogue.confirm(translator.translate('profile.blocks.unblock.confirm', { username: block.to_username }), 'danger');
await dialogue.postWithAlertOnError(`/api/user/unblock/${block.to_userId}`);
await blocks.refresh();
};
</script>
<template>
<Loading :value="blocks.data.value">
<ul v-if="blocks.data.value">
<li v-if="blocks.data.value.length === 0">
<T>profile.blocks.empty</T>
</li>
<li v-for="block in blocks.data.value" :key="block.id">
@{{ block.to_username }}
<small class="text-muted">({{ $datetime($ulidTime(block.id)) }})</small>
<a href="#" :aria-label="$t('profile.blocks.unblock.action')" class="btn btn-link btn-sm" @click.prevent="unblock(block)">
<Icon v="trash" />
</a>
</li>
</ul>
<p class="small mb-0">
<Icon v="info-circle" /> <T>profile.blocks.instruction</T>
</p>
</Loading>
</template>

View File

@ -60,7 +60,12 @@ const icsLink = computed(() => {
<T>calendar.image.header</T><T>quotation.colon</T>
</p>
<p v-if="day" class="mb-0">
<a :href="`/calendar/${day}.png`" target="_blank" rel="noopener" class="btn btn-outline-primary m-1">
<a
:href="`/calendar/${config.locale}/${day}.png`"
target="_blank"
rel="noopener"
class="btn btn-outline-primary m-1"
>
<Icon v="image" />
<T>calendar.image.header</T>
</a>

View File

@ -306,7 +306,7 @@ const dismissCensus = (): void => {
</template>
</PotentiallyExternalLink>
</template>
<span class="nav-item btn btn-sm position-relative" @click="searchDialogue?.open()">
<button type="button" class="nav-item nav-header btn btn-sm position-relative" @click="searchDialogue?.open()">
<span style="position: absolute; right: 0.1em; top: 0.1em; opacity: 0.7; z-index: -1">
<ClientOnly>
<kbd class="bg-light text-dark border">{{ isMac ? '⌘K' : 'Ctrl+K' }}</kbd>
@ -315,7 +315,7 @@ const dismissCensus = (): void => {
<Icon v="search" :size="1.6" />
<br>
<T>search.header</T>
</span>
</button>
<div class="nav-item flex-grow-0" @mouseenter="hoverItem = null">
<VersionDropdown end />
</div>
@ -370,10 +370,10 @@ const dismissCensus = (): void => {
<Spelling :text="link.textLong || link.text" />
</PotentiallyExternalLink>
</template>
<span class="btn btn-sm" @click="searchDialogue?.open()">
<button type="button" class="btn btn-sm" @click="searchDialogue?.open()">
<Icon v="search" />
<T>search.header</T>
</span>
</button>
</div>
</div>
</div>

View File

@ -1,6 +1,8 @@
<script setup lang="ts">
import { fontHeadingsByLocale } from '~/locale/fonts.ts';
import type { LocaleDescription } from '~/locale/locales.ts';
import { getUrlForLocale } from '~/src/domain.ts';
import { formatFonts } from '~/src/fonts.ts';
withDefaults(defineProps<{
locales: Record<string, LocaleDescription>;
@ -19,6 +21,7 @@ withDefaults(defineProps<{
:key="locale"
:href="getUrlForLocale(locale)"
class="list-group-item list-group-item-action list-group-item-hoverable w-md-50 border-start"
:style="`--font-headings: ${formatFonts(fontHeadingsByLocale[locale])}`"
>
<div class="h3">
<LocaleIcon :locale="options" class="mx-2" />

View File

@ -1,3 +1,11 @@
<script setup lang="ts">
const event = useRequestEvent();
if (event) {
event.node.res.statusCode = 404;
}
</script>
<template>
<div>
<h2>
@ -5,6 +13,10 @@
<T>notFound.message</T>
</h2>
<section>
<SearchForm />
</section>
<p class="h4 mt-4">
<nuxt-link to="/">
<Icon v="chevron-circle-left" />

View File

@ -45,11 +45,11 @@ const { floatingStyles, placement: calculatedPlacement, middlewareData } = useFl
const visible = ref(false);
const slots = defineSlots<{
default(): VNode;
content(): VNode[];
default: () => VNode;
content?: () => VNode[];
}>();
const hasContent = computed((): boolean => {
return !!slots.content()[0];
return !!slots.content?.()[0];
});
const arrowStyles = computed((): CSSProperties | undefined => {

View File

@ -1,62 +1,16 @@
<script setup lang="ts">
import { onKeyStroke, useDebounce } from '@vueuse/core';
import { normaliseQuery, validateQuery } from '~/src/search.ts';
const query = ref('');
const normalisedQuery = computed(() => {
return normaliseQuery(query.value);
});
const debouncedQuery = useDebounce(normalisedQuery);
const queryValidation = computed(() => {
return validateQuery(normalisedQuery.value);
});
const searchAsyncData = useAsyncData('search', async () => {
selected.value = null;
if (queryValidation.value !== undefined) {
return [];
}
return await $fetch('/api/search', {
query: {
query: debouncedQuery.value,
},
});
}, {
watch: [debouncedQuery],
});
const isLoading = computed(() => {
return queryValidation.value === undefined &&
(normalisedQuery.value !== debouncedQuery.value || searchAsyncData.status.value === 'pending');
});
const hasNoResults = computed(() => {
return !isLoading.value && searchAsyncData.data.value?.length === 0;
});
const dialogue = useTemplateRef('dialogue');
const searchForm = useTemplateRef('searchForm');
const open = () => {
query.value = '';
searchAsyncData.clear();
searchForm.value?.reset();
dialogue.value?.showModal();
selected.value = null;
};
const close = () => {
dialogue.value?.close();
};
onKeyStroke('k', (event) => {
const isCtrlKeyPressed = isMac.value ? event.metaKey : event.ctrlKey;
if (isCtrlKeyPressed) {
event.preventDefault();
open();
}
});
const onMousedown = (event: MouseEvent) => {
const rect = dialogue.value?.getBoundingClientRect();
if (rect && (
@ -67,81 +21,11 @@ const onMousedown = (event: MouseEvent) => {
}
};
const selected = ref<number | null>(null);
const router = useRouter();
onKeyStroke('ArrowDown', () => {
selected.value = Math.min(
(selected.value ?? -1) + 1,
(searchAsyncData.data?.value?.length ?? 0) - 1,
);
});
onKeyStroke('ArrowUp', () => {
selected.value = Math.max(
(selected.value ?? -1) - 1,
0,
);
});
onKeyStroke('Enter', () => {
if (selected.value === null) {
return;
}
const document = searchAsyncData.data.value?.[selected.value];
if (!document) {
return;
}
router.push(document.url);
close();
});
defineExpose({ open, close });
</script>
<template>
<dialog ref="dialogue" class="container m-auto h-100 rounded border" @mousedown="onMousedown">
<div class="input-group mb-4">
<span class="input-group-text">
<Spinner v-if="isLoading" size="1.25em" />
<Icon v-else v="search" />
</span>
<input
v-model="query"
type="search"
class="form-control border-primary"
:placeholder="$t('crud.search')"
>
<button class="btn btn-outline-danger" @click="close()">
<Icon v="times" />
</button>
</div>
<div v-if="queryValidation !== undefined" class="alert alert-info">
<T>search.{{ queryValidation }}</T>
</div>
<ul
v-else-if="searchAsyncData.data.value?.length ?? 0 > 0"
class="list-group"
>
<li
v-for="(document, index) of searchAsyncData.data.value"
:key="`${document.kind}-${document.id}`"
:class="['list-group-item list-group-item-action p-0', selected === index ? 'list-group-item-active' : '']"
>
<SearchItem :document="document" @click="close()" />
</li>
</ul>
<div v-else-if="hasNoResults" class="alert alert-info">
<T :params="{ query }">search.noResults</T>
</div>
<SearchForm ref="searchForm" @selected="close()" />
</dialog>
</template>
<style lang="scss">
@import "assets/variables";
.list-group-item-active {
background-color: $list-group-hover-bg !important;
border-left: 3px solid $primary !important;
margin-inline-start: -2px; /** compensate for the border mark, minus 1px (regular border) */
}
</style>

View File

@ -0,0 +1,124 @@
<script setup lang="ts">
import { onKeyStroke, useDebounce } from '@vueuse/core';
import { normaliseQuery, validateQuery } from '~/src/search.ts';
const emit = defineEmits<{
selected: [];
}>();
const query = ref('');
const normalisedQuery = computed(() => {
return normaliseQuery(query.value);
});
const debouncedQuery = useDebounce(normalisedQuery);
const queryValidation = computed(() => {
return validateQuery(normalisedQuery.value);
});
const searchAsyncData = useAsyncData('search', async () => {
selected.value = null;
if (queryValidation.value !== undefined) {
return [];
}
return await $fetch('/api/search', {
query: {
query: debouncedQuery.value,
},
});
}, {
watch: [debouncedQuery],
});
const isLoading = computed(() => {
return queryValidation.value === undefined &&
(normalisedQuery.value !== debouncedQuery.value || searchAsyncData.status.value === 'pending');
});
const hasNoResults = computed(() => {
return !isLoading.value && searchAsyncData.data.value?.length === 0;
});
onKeyStroke('k', (event) => {
const isCtrlKeyPressed = isMac.value ? event.metaKey : event.ctrlKey;
if (isCtrlKeyPressed) {
event.preventDefault();
open();
}
});
const selected = ref<number | null>(null);
const searchItems = useTemplateRef('searchItems');
onKeyStroke('ArrowDown', () => {
selected.value = Math.min(
(selected.value ?? -1) + 1,
(searchAsyncData.data?.value?.length ?? 0) - 1,
);
});
onKeyStroke('ArrowUp', () => {
selected.value = Math.max(
(selected.value ?? -1) - 1,
0,
);
});
onKeyStroke('Enter', () => {
if (selected.value === null) {
return;
}
const document = searchAsyncData.data.value?.[selected.value];
if (!document) {
return;
}
const link = searchItems.value?.[selected.value]?.firstElementChild as HTMLElement | undefined;
link?.click();
});
const reset = () => {
query.value = '';
searchAsyncData.clear();
selected.value = null;
};
defineExpose({ reset });
</script>
<template>
<div class="input-group mb-4">
<span class="input-group-text">
<Spinner v-if="isLoading" size="1.25em" />
<Icon v-else v="search" />
</span>
<input
v-model="query"
type="search"
autofocus
class="form-control border-primary"
:placeholder="$t('crud.search')"
>
<button class="btn btn-outline-danger" @click="emit('selected')">
<Icon v="times" />
</button>
</div>
<div v-if="queryValidation !== undefined" class="alert alert-info">
<T>search.{{ queryValidation }}</T>
</div>
<ul
v-else-if="searchAsyncData.data.value?.length ?? 0 > 0"
class="list-group"
>
<li
v-for="(document, index) of searchAsyncData.data.value"
:key="`${document.kind}-${document.id}`"
ref="searchItems"
:class="['list-group-item list-group-item-action py-0 pe-0', selected === index ? 'list-group-item-active' : '']"
>
<SearchItem :document="document" @click="emit('selected')" />
</li>
</ul>
<div v-else-if="hasNoResults" class="alert alert-info">
<T :params="{ query }">search.noResults</T>
</div>
</template>

View File

@ -31,7 +31,7 @@ const searchKindHasImage = computed(() => {
</script>
<template>
<nuxt-link :to="document.url" class="d-block p-3 text-dark text-decoration-none">
<PotentiallyExternalLink :to="document.url" class="search-item d-block p-3 text-dark text-decoration-none">
<div class="h3 mb-3">
<Icon :v="icon" />
<Spelling :text="document.title" />
@ -67,5 +67,11 @@ const searchKindHasImage = computed(() => {
</div>
</div>
<Spelling v-else :text="document.content" />
</nuxt-link>
</PotentiallyExternalLink>
</template>
<style scoped lang="scss">
.search-item {
margin-inline-start: -1rem;
}
</style>

View File

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1.4
FROM node:20 as base
FROM node:22.14.0 as base
RUN apt-get update && apt-get install -y openssh-client tini sudo

View File

@ -34,7 +34,7 @@ useHead({
<summary class="bg-light p-3">
Additional Information
</summary>
<div class="p-2" v-html="error.stack"></div>
<pre class="p-2" v-html="error.stack"></pre>
{{ error.cause }}
</details>

View File

@ -160,7 +160,6 @@ export default withNuxt(
'@stylistic/implicit-arrow-linebreak': 'warn',
'@stylistic/linebreak-style': 'warn',
'@stylistic/max-len': ['warn', { code: 120 }],
'@stylistic/newline-per-chained-call': 'warn',
'@stylistic/no-extra-semi': 'warn',
'@stylistic/no-mixed-operators': ['warn', {
groups: [['&&', '||']],

View File

@ -8,7 +8,7 @@ import useDark from '~/composables/useDark.ts';
import useDialogue from '~/composables/useDialogue.ts';
import { longtimeCookieSetting } from '~/src/cookieSettings.ts';
import { LoadScriptError } from '~/src/errors.ts';
import { executeUnlessPrerendering, newDate } from '~/src/helpers.ts';
import { executeUnlessPrerendering } from '~/src/helpers.ts';
import { useMainStore } from '~/store/index.ts';
// no need to be super secure, just a sign that the page is not public
@ -48,7 +48,6 @@ onMounted(executeUnlessPrerendering(() => {
confirmAge();
loadAds();
loadGTM();
let needsRefresh = false;
const bc = new BroadcastChannel('account_switch');
@ -100,12 +99,6 @@ const loadAds = async (): Promise<void> => {
'publift',
'https://cdn.fuseplatform.net/publift/tags/2/3329/fuse.js',
);
await loadScript(
'publift-video',
'https://live.primis.tech/live/liveView.php?s=118558&schain=1.0,1!publift.com,[01H9H7XDCTSKKX1ECPR1VWQXQ9],1',
undefined,
'[data-phkey=content-0]',
);
} catch (error) {
if (error instanceof LoadScriptError) {
return;
@ -113,27 +106,6 @@ const loadAds = async (): Promise<void> => {
throw error;
}
};
const loadGTM = async (): Promise<void> => {
if (!adsEnabled.value) {
return;
}
try {
await loadScript('gtm', 'https://www.googletagmanager.com/gtag/js?id=G-TDJEP12Q3M');
} catch (error) {
if (error instanceof LoadScriptError) {
return;
}
throw error;
}
window.dataLayer = window.dataLayer || [];
function gtag(...args: unknown[]): void {
window.dataLayer.push(args);
}
gtag('js', newDate());
gtag('config', 'G-TDJEP12Q3M');
};
</script>
<template>

View File

@ -850,6 +850,20 @@ profile:
action: 'Remove yourself'
confirm: 'Are you sure you want to remove your profile from @%username%''s circle?'
add: 'Add people to your circle'
blocks:
header: 'Blocked accounts'
action: 'Block this person'
confirm: >
Are you sure you want to block @%username%?
You will not be able to see each other's cards or add each other to your circles.
This can be reversed in your Account page.
unblock:
action: 'Unblock this person'
confirm: >
Are you sure you want to unblock @%username%?
You will be able to see each other's cards or add each other to your circles.
empty: 'You currently do not have any accounts blocked.'
instruction: 'You can block a person by visiting their profile.'
sensitive:
header: 'Content warning'
info: >

View File

@ -4,6 +4,15 @@
![@JuliaRidsBikes على تويتر: "لماذا للناس المولودين بجنسهم الصحيح جنس وأنا لدي هوية جنس؟"؛ @we_are_biscuit على تويتر: "نفس السبب أنهم "يكونون" محبين للجنس الآخر لكني "أعرّف نفسي" كمزدوج التوجه. يحبون جدًا أن يقللوا من شأننا."](/img/ar/blog/cis-gender-identity.png)
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/identify-as" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
إحدى الأمور التي تزعجني عندما يتعلق الأمر بمناقشة الهويات المثلية هي عبارة "أُعَرِّف نفسي كـ...".
لأنها على الرغم من أنها صحيحة تقنيًا وغالبًا ما تُستخدم بنوايا حسنة للغاية،
إلا أنها في الغالب تُعطي الناس انطباعات خاطئة عن ماهية الهوية.

View File

@ -4,6 +4,18 @@
![لقطة شاشة لعناوين منشورات "رديت"، كلها مقتبسة أدناه](/img/ar/blog/can-i-be.png)
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/can-i-be" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
<li class="list-inline-item">
<a href="https://pronombr.es/blog/puedo-ser" target="_blank" rel="noopener" class="badge text-bg-info">Español</a>
</li>
</ul>
لقد لاحظت مؤخرًا أن عدد المشاركات في بعض المنتديات الفرعية التي أتابعها هي في الأساس طلبات للتحقق من هوية الشخص.
فيما يلي بعض الأمثلة من واحد منها فقط ([r/Nonbinary](https://reddit.com/r/Nonbinary)):

View File

@ -0,0 +1,84 @@
# البقاء على قيد الحياة في العالم الحديث يبدأ بالتعاون مع بعضنا البعض. وجهة نظر شخصية بولندية من المثليين الذين ينظمون حياتهم بأنفسهم من أجل حياة أفضل.
<small>2025-02-09 | [@T_Vos](/@T_Vos)، مُترجَمة من قِبَل الفريق العربي.</small>
![صورة بتدرج رمادي لشخص جالس على الأرض، وظهره للكاميرا. اللون الوحيد الذي يبرز هو قوس قزح خلف ظهره.](/img/en/blog/rainbow-gray.jpeg)
<div class="alert alert-warning">
<span class="fal fa-exclamation-triangle"></span>
<strong>تحذيرات المحتوى:</strong>
ذكر خفيف للكراهية ضد المثليين والإساءة
</div>
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/rural-polish-queer-experience" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
الأوقات صعبة بالنسبة لمجتمعنا. نحن نصبح مثال لأنواع عديدة من الكائنات الشريرة في هذا العالم.
لماذا نحن؟
نحن هدف مثالي لدعاية اليمين المتطرف. مجموعة كبيرة بما يكفي لكي يصادفنا ضحايا الدعاية في الشوارع، لكنها ليست كبيرة بما يكفي لإحداث فوضى أو الإطاحة بروايتهم ومع ذلك نحن كبيرون بما يكفي للقتال بحيث يمكنهم انتقاد كلماتنا ورميها ضدنا.
كل هذا صحيح وكان صحيحًا في الماضي. حتى قبل "الذعر المتعلق بالمتحوليين"، لم تكن الحقيقة جيدة بالنسبة لمجتمعنا. كنا منبوذين، واحتياجاتنا غير مرئية، ونقص الحقوق كان يُستهزأ به. في ذلك الوقت، أراد بعضنا "أن نبقى في الظل ولا نأخذ مكانًا" على أمل أن يجعل هذا مضطهدينا كسالى وغير راغبين في متابعة التمييز النشط.
كان هؤلاء الأشخاص مخطئين. _نحن_ نرى ذلك بوضوح الآن.
الجلوس بهدوء لا يجعل متنمرك يذهب بعيدًا، الوقوف في وجههم، والدفاع عن نفسك والفخر _رغم_ الألم هو ما جعل الثورة الجنسية ناجحة في العقود الأخيرة. هو ما جعلني _أكون_ ناجحًا وقادرًا على الازدهار، لكي أكون هنا معكم اليوم لتقرأوا هذا.
وُلدت في ريف بولندا. على بعد ساعة بالسيارة من الساحل، في مدينة تعداد سكانها 30,000 محاطة بالفراغ، حيث قضيت طفولتي ومراهقتي. كنت أول مولود في عائلة محافظة جدًا، كاثوليكية، وأسرية. لم يكن هناك أي مجتمع رسمي في دائرة نصف قطرها 100 كم على الأقل. لا مجموعة دعم، لا مجموعات على الإنترنت، لا منظمات أو فعاليات، لا فخر، لا شيء لموازنة الدعاية الرومانية الكاثوليكية التي كانت تأتي إليّ من الكنيسة والمدرسة والعائلة والتلفزيون ومجلس المدينة. عندما تم الكشف عن ميولي الجنسية في المدرسة الإعدادية، سمعت من المعلمين أن "لو كنت تؤمن بالله، كنت سأُخلَّص في الحياة الآخرة" كعلاج للمتنمرين الحقيقيين الذين كانوا يقفون خارج غرفة الاستشارة. لم يتم حتى توبيخهم على سلوكهم. لم يكن من الممتع أن تكبر وأنت مثلي في تلك المدينة.
ومع ذلك، عندما حان الوقت لاستكشاف ما تعنيه ميولي الجنسية بالنسبة لي، اكتشفت أنني لست وحدي في ما أنا عليه، وأن هناك الكثير منا حتى في مدينتي الصغيرة.
فكيف يجد المرء مجتمعًا في مكان يائس؟
بما أنه لم تكن هناك منظمات ولا سياسيين فكروا في تقديم أي شيء لنا، اتجهنا إلى بعضنا البعض. لم يكن هناك مساحة آمنة للمثليين، لذا أنشأنا واحدة بشكل عضوي. تشكلت العديد من المجموعات الصغيرة غير الرسمية مثل مجموعات الأصدقاء، وبدأ الناس في التجمع في المجموعات الصغيرة. قضينا الوقت مثلما يفعل الأصدقاء، لكن لم نكن جميعًا نحب بعضنا البعض. كنتم تعلمون أنكم موجودون لبعض الأشخاص الذين كانوا أصدقائكم، وكان هؤلاء الأشخاص دائمًا يظلون قريبين منكم، ولكنكم قضيتم أيضًا الكثير من الوقت مع مثليين آخرين، كانوا فقط في محيطكم ولكن لم يكن هناك التزام بأن تكونوا صديقًا قريبًا لهم.
مشاعر حقيقية للـ"عائلة المختارة". ولكن حيث يتم تمديد العائلة المختارة إلى المجتمع بأسره. كمثليـ/ـة كان لديكم دائمًا مكان في هذه المجموعات، بعضها كان أكثر تحفظًا من البعض الآخر، وبعضها كان فنيًا جدًا، وبعضها كان علميًا، … ولكن تلك المساحة المفتوحة كانت دائمًا موجودة.
قبل أن أجد مجموعتي، تنقلت عدة مرات بين الأشخاص بحثًا عن قبيلتي. كان ذلك جزءًا طبيعيًا من العملية، وكان الناس يتوقعون أن تتنقل عندما يصبح من الواضح أن طاقتك لا تتناسب مع باقي المجموعة. ولكن حتى في ذلك الحين، كان هناك تضامن، ولم يكن يتم دفعك للخارج ما لم تكن فعلاً مميزًا لوقت طويل. كان هناك شعور بمجتمع أوسع بيننا. كنا نعلم أننا فقط نحب بطريقة مختلفة وأننا أشخاص طبيعيون بخلاف ذلك. ولكن تلك الفروق الصغيرة أحدثت _كل_ الفرق.
عندما كبرت أخيرًا وكنت قادرًا على مغادرة مكان ولادتي، فعلت ذلك. تركنا جميعًا تقريبًا. كان من المفترض أن ننتقل معًا ولكنها انهارت سريعًا كما يحدث عادةً عندما تنتقلون جميعًا. خاصة عندما ترميك الحياة في مدن مختلفة. وبدأت على الفور في القيام بنفس الشيء في مكاني الجديد. كنت أشتاق لذلك الفضاء الآمن المألوف للمثليين. انجذبت نحو الأشخاص الذين بدا أنهم قادة في المجتمع، الآن المجتمع الحقيقي، المفتوح والوجودي. لقد أظهروا لي قواعد الحياة في المدينة الكبيرة وما هو موجود داخل المجتمع. كنت لا أزال أعيش في بولندا المحافظة والمعادية للمثليين، لكنني كنت راضيًا لأنني كنت أعلم أنني لست وحدي.
علمتنا هذه المجموعات غير الرسمية التضامن والدعم المتبادل، والاحترام، والكرامة، وعدم الاستقرار.
وقد يكون هذا ما نحتاجه أكثر هذه الأيام.
بالطبع، نحتاج إلى محاربة الاضطهاد ولكن ليس بإمكان الجميع أن يكونوا جنودًا في الخط الأمامي. ولكن تمامًا كما يحتاج الجيش الحقيقي إلى شخص يطعمهم، ويلبسهم، ويكتب لهم البرمجيات، ويعتني بمنزلهم أثناء غيابهم، يحتاج متمردونا نفس الشيء!
لا تستهينوا بقوة الجماعة! لقد رأيت ذلك يعمل بشكل جيد على مدار السنوات الأربع الماضية على موقعنا، وجعلني أفكر في ماضيّ. وأدركت أن الطريقة التي نشأ بها هذا المشروع كانت تمامًا مثل تلك المجموعات الداعمة غير الرسمية في مكان ولادتي. كان لدى شخص ما حاجة لم يتم تلبيتها من قبل العالم الموجود حولنا. لذلك، جلسوا لصنعها واقعًا. ثم رأى الآخرون في المجتمع ذلك وأرادوا البناء عليه. شخصًا تلو الآخر بدأنا في النمو، وظهرت لغات جديدة تدريجيًا، وسحب البطاقات الشخصية جلب المزيد من الأشخاص، ونتيجة لذلك، بدأ المزيد من الأشخاص في المساهمة وكنّا مثل كرة الثلج في النمو، ثم بدأنا في مساعدة الترجمة، وتعليم الشمول والتنوع والمساواة، ثم توسعنا وبدأنا في مساعدة الأكاديميا مع تحدياتهم. الأشخاص يأتون ويذهبون كما فعلوا في مكان ولادتي. نساعد بعضنا البعض ونحاول رفع البعض بقدر ما نستطيع وبقدر ما هو معقول. لا توجد مشاعر قاسية عندما تدفعك الحياة بعيدًا. هناك فهم. نحن نعلم أننا جميعًا مجرد بشر، نحن جميعًا مرتبكون وأحيانًا خائفون. ونحن نعلم أنه في بعض الأحيان تجعل الحياة بعض الالتزامات غير قابلة للاستمرار، حتى لو كنت تستمتع بها. أنا أشارك كل هذا كمثال على التفكير الجماعي. نعمل جميعًا كواحد ولكن كما لا يتساءل ساقيك أين يوجههم دماغك، نحن لا نتساءل عن حياة الآخرين. الثقة في بعضنا البعض هي المفتاح.
فقط لأن هذه جماعة مفتوحة، لا يعني أنه لا توجد متطلبات للانضمام إليها. وكما في مكان ولادتي، هذا الموقع يستمر فقط لأننا جميعًا نضع العمل نحو مجتمعنا، عائلتنا. جزء مهم من هذا المزيج هو عدم تقبل هراء الآخرين ولكن التصدي له.
الآن، من المهم أن نكون واقعيين. هل ستعالج جماعة مثليين قوية جميع مشاكلنا؟ لا.
قد تقصر حتى عن إصلاح معظمها. ولكن لكي نتمكن من مواجهة جميع التحديات التي تلقيها الحياة علينا؛ نحتاج إلى شاطئ مستقر يمكننا أن نرسو فيه. نحتاج إلى مكان نسميه منزلًا، سببًا للقتال من أجله.
في أيامنا تلك، كان منتدى الإنترنت التقليدي في أواخر التسعينيات هو كل ما كنت أتمناه. وقد نجحنا. قبل أيام طفولتي، كانت الأجيال السابقة من المثليين والمثليات لديهم موارد أقل بكثير. ومع ذلك، نجحوا. الآن مع كل الأدوات في العالم، أصبح بناء المجتمعات أسهل من أي وقت مضى. وهي تجني الفوائد! العديد من أقراني من مجموعاتي الآن يعيشون حياة مهنية رائعة، حياتهم الخاصة، عائلاتهم المثليّة السعيدة ومستقبلاتهم. بما فيهم أنا. هذا الأساس الذي منحني إياه المجتمع سمح لي بالهروب من الإساءة والفوبيات إلى حياة حرة ومزدهرة. _يمكنك أيضًا أن تحصلوا على كل شيء_. ولكن ليس عندما لا تنجو أنتم والأشخاص الذين يعانون معكم في هذه العاصفة الفاشية.
---
الآن أنا أدعوكم!
أدعوكم لقصتكم! أدعوكم لصوتكم!
تحدثوا وامكنوا بعضكم البعض!
يمكننا الاستسلام أو يمكننا الازدهار. عمدًا في نهاية هذه المقالة أضع دعوة للعمل.
مؤخرًا نشرنا قصصًا لبعض أعضاء فريقنا، كيف تمكنوا من التنقل في حياتهم كمثليين
[مسيحيين](/المدونة/المثليين-المسيحيين) و[كأشخاص غير ثنائيين في العالم العربي](/المدونة/خارج-الثتائية).
حدثونا عن كيفية رفعكم لبعضكم البعض في مجتمعكم المحلي، وكيف تقدرون البقاء على قيد الحياة والازدهار في وجه خصومكم! أخبرونا عن نضال المثليين وحياتهم في منطقتكم من العالم!
صندوق بريدنا [contact@pronouns.page](mailto:contact@pronouns.page) مفتوح لتقديم قصص من تجربتكم الحياتية.
نود قراءتها وربما نشر قصتكم. يمكنكم إبقاءها مجهولة أو نشرها باسمكم، الخيار لكم!
لكننا نريد سماع قصتكم ونقلها إلى الملايين الذين يزورون هذه الصفحة سنويًا.
شكرًا لقراءتكم هذا! أقدّر لكم إعطائي الوقت، لأن هذا جزء من قصتي وحياتي.
نحبكم! استمروا في كونكم مثليين!

View File

@ -2,6 +2,15 @@
<small>2022-04-23 | [@andrea](/@andrea)، مُترجَمة من قِبَل الفريق العربي.</small>
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/facebook-login-deprecated" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
نعلم جميعًا أن الفيسبوك لا يهتم بالأخلاق أو خصوصية المستخدمين.
ولكن ما لا يعلمه البعض هو أنهم أيضًا غير ودودين تجاه المطورين ويحاولون فرض وجهات نظرهم المحافظة والحكيمة على مواقع الويب المستقلة؟

View File

@ -4,6 +4,15 @@
![تجميع لمجموعة من الطرق المذكورة لاحقًا في المقالة (قائمة باللغات مع حروف الهجاء، خريطة، أعلام دولينجو، أعلام مجمعة...))](/img/ar/blog/graphical-languages/graphical-languages.png)
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/graphical-languages" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
يعد وضع العناصر الجرافيكية بجانب التسميات النصية خدعة تصميمية رائعة - فهي تساعد عقلك على تخطي بعض الخطوات عند
التنقل في واجهة المستخدم: فبدلًا من قراءة التسمية يتمكن معرفة ما يفعله الزر على الفور بمجرد إلقاء نظرة على الأيقونة.

View File

@ -0,0 +1,91 @@
# العيش كشخص مسيحي مثلي
<small>2025-01-02 | [@Its_LilFroggo](/@Its_LilFroggo)، مُترجَمة من قِبَل الفريق العربي.</small>
<div class="alert alert-info">
<span class="fal fa-info-circle"></span>
<strong>هذه مقالة رأي كتبها أحد أعضاء جماعة موقعنا</strong>
</div>
<div class="alert alert-warning">
<span class="fal fa-exclamation-triangle"></span>
<strong>تحذير من المحتوى:</strong>
تتناول هذه التدوينة التعصب الديني ضد مجتمع المثلية
</div>
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/lgbtq-christians" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
العيش كمسيحي ضمن مجتمع المثلية هو بالتأكيد أمر صعب، ولكنه ليس مستحيلاً.
لم يكن الأمر دائمًا كذلك، وبصراحة، لم يكن من السهل رؤية مشاعري على حقيقتها حتى حصلت على حرية التفكير.
النشأة كمسيحي تعني أنك تمر بتعاليم الكتاب المقدس أو الأسرار المقدسة وفقًا للتقليد الذي نشأت عليه...
كاثوليكي، أرثوذكسي، أنجليكاني، لوثري. في النهاية، تكمل تعليمك الديني ويتم إرسالك إلى طريقك.
بالنسبة لي، لم تكن دراستي الدينية جيدة جدًا، لذا توقفت عن الممارسة بشكل طبيعي.
سمح لي هذا باستكشاف نفسي خارج هذه الحدود والقواعد التي كنت أعتقد أنها كل ما تعنيه الإيمان.
والآن قبل أن تظن أن هذه القصة ستكون مثل الأفلام حيث تدرك أنك مثلي منذ البداية، فأنت مخطئ.
في الواقع، عندما شعرت لأول مرة بإعجاب تجاه نفس الجنس، شعرت بالخوف.
بالتأكيد، كنت ملحدًا، لكن المبادئ الأخلاقية التي نشأت عليها كانت لا تزال قائمة. شعرت بالوحدة.
لم أكن متأكدًا مما كنت أشعر به بالضبط، وسأقول فقط: **ملستم لوحدك**.
المشاعر؟ الهوية؟ لكل شخص قصة مختلفة عن كيفية اكتشافه لحقيقته.
ربما تفضل التواجد مع الجنس الآخر وتتجنب التفاعل مع نفس الجنس لأنك تدرك بشكل غير واعٍ أنك ستطور مشاعر تجاههم...
أو ربما شعرت بالغيرة أو الغضب عندما رأيت امرأة، أو رجلًا، أو شخصًا غير ثنائي يقبل شخصًا كنت معجبًا به...
أو ربما تستمتع بارتداء ملابس أكثر أنوثة أو ذكورة...
لكل شخص قصة مختلفة. البعض يندمج ويبقى في الخزانة، والبعض الآخر أكثر انفتاحًا.
من الصحيح أن المسيحيين قد يكونون أكثر تحفظًا بسبب الخوف من الاضطهاد من مجتمعهم الديني أو أولياء أمورهم أو حتى أصدقائهم وزملائهم.
**لكن هذا ليس ما ينبغي أن يكون عليه الأمر.**
## لماذا لا يقبلني المتدينون؟
**يجب أن يقبلوكم.** إذا قرأتم الأناجيل، ستدركوا أنه عندما قال يسوع أن تحب قريبك،
كان يقصد الجميع، وهذا يشمل مجتمع المثلية وأولئك الذين يعانون من أمراض عقلية أو جسدية وإعاقات، وأشخاص من أعراق مختلفة، وما إلى ذلك.
لا تصدقوني؟ لننظر إلى قصة المرأة التي أُمسكت في الزنا في يوحنا 8:1-11.
في هذه القصة، يُحضر القادة الدينيون امرأة أُمسكت في الزنا إلى يسوع لمحاولة الإيقاع به.
وفقًا لشريعة موسى، كان يجب رجمها، لكنهم أرادوا رؤية ما إذا كان يسوع سيتحدى القانون.
بدلًا من إدانتها أو الدخول في نقاشهم، قال يسوع بهدوء: "من كان منكم بلا خطيئة، فليرمها بأول حجر."
واحدًا تلو الآخر، غادر المتهمون، حتى بقي يسوع والمرأة وحدهما. فقال لها يسوع:
"ولا أنا أدينك. اذهبي ولا تخطئي بعد الآن."
في هذه القصة، من المهم التركيز على كلمات يسوع **"ولا أنا أدينك"**.
بالتأكيد، الزنا مكروه وفقًا للتقاليد المسيحية، ولكن لا يحق لأي مسيحي أن يحكم عليك أو يدينك.
**فقط الله هو القاضي.**
و **مالله لا يدينكم. إنه يحبك.** لا تصدقوني؟ متى 9:10-13.
"وبينما كان يسوع يتناول الطعام في بيت متى، جاء كثير من جباة الضرائب والخطاة وأكلوا معه ومع تلاميذه.
وعندما رأى الفريسيون ذلك، قالوا لتلاميذه: ‘لماذا يأكل معلمكم مع جباة الضرائب والخطاة؟’
فلما سمع يسوع ذلك، قال: ‘ليس الأصحاء بحاجة إلى طبيب، بل المرضى.
لكن اذهبوا وتعلموا ما معنى: ‘إني أريد رحمة لا ذبيحة. فإني لم آت لأدعو الأبرار، بل الخطاة."
كان يسوع يتعامل مع الخطاة عمدًا، مما يظهر أن مهمته كانت الرحمة والشفاء والفداء بدلاً من الحكم.
يجب على المسيحيين أن يعاملوا أفراد مجتمعنا بنفس الطريقة.
## كيف أعيش كمسيحي مثلي؟
أولاً وقبل كل شيء، اعلموا أنكم محبوبون من الله كما أنتم.
هويتكم كمثليون ليست عائقًا أمام محبته، بل جزء من الشخص الفريد الذي خلقه.
تؤكد الكتابات المقدسة ذلك مرارًا وتكرارًا. روما 8:38-39:
"لأني مقتنع بأنه لا الموت ولا الحياة، ولا الملائكة ولا الشياطين... ولا أي شيء آخر في كل الخليقة،
سيكون قادرًا على أن يفصلنا عن محبة الله التي في المسيح يسوع ربنا."
لا شيء، أكرر لا شيء، يمكن أن يفصلكم عن محبته. **الله يحبكم رغم أي قواعد أو أحكام يصدرها الناس ضدكم.** أنتم لستم لوحدكم.
هناك العديد من المسيحيين الذين يشاركونكم رحلتكم ويمكنهم تقديم الدعم، سواء عبر الإنترنت أو في مساحات مرحبة محليًا.
تشمل بعض الأمثلة: The Reformation Project، Q Christian Fellowship، Believe Out Loud، The Gay Christian Network،
Inclusive Church، Reconciling Ministries Network (RMN)، وغيرهم.
ومع ذلك، قد تكون هناك مجتمعات أكثر تعصبًا. لا تجعلوا منها معيارًا لكيفية تصرف جميع المسيحيين تجاه المجتمع، لأن هذا ببساطة غير صحيح.
**ليس عليكم البقاء في أماكن تضر بصحتكم العقلية أو الروحية.**
لا بأس في الابتعاد عن العلاقات أو المجتمعات التي تجعلكم تشعروا بعدم الترحيب أو بعدم الحب.
لكن من الجيد أيضًا الاستمرار في حضور الخدمات إذا كانت تمنحكم السلام والراحة، فقط تذكروا أن تصلوا من أجل أولئك الذين يحكمون عليك.
وتذكروا أيضًا أن الله سيحاسبهم على أعمالهم! يحبنا الله جميعًا، ونحن مدعوون لأن نحب بعضنا بعضًا، بغض النظر عن اختلافاتنا.
## الخاتمة
رحلتكم كمسيحيين مثليين هي شهادة على محبة الله اللامحدودة وإبداعه.
احتضنوا هويتكم وإيمانكم كنعمتين تجعلانكم مميزين، ولا تكرهواأنفسكم فقط لأن المجتمع لا يريد قبولكم.
أنتم محبوبون من الله، تذكروا ذلك. من الطبيعي أن تواجهوا صعوبات في رحلتكم الإيمانية، لكن هذا لا بأس به.
الإيمان هو رحلة، ومع كل خطوة إلى الأمام، تُظهر أن المحبة محبة الله يمكن أن تتغلب على الخوف والأحكام.
أخيرًا، اعلم أنكم تستحقون القبول، وإذا لم تجدونه، **فتذكروا أنكم لستم لوحدكم، وأنكم محبوبون ومقبولون كما أنتم.**

View File

@ -10,6 +10,18 @@
الإشارة إلى الأعضاء التناسلية والقمع البطريركي والشتائم والعنف ضد المثليين
</div>
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/three-models" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
<li class="list-inline-item">
<a href="https://zaimki.pl/blog/trzy-modele" target="_blank" rel="noopener" class="badge text-bg-info">Polski</a>
</li>
</ul>
لقد أرسل لنا شخص ما بريدًا إلكترونيًا، يطلب فيه توضيحًا لبعض التعريفات من [قاموس المصطلحات المثلية](/المصطلحات). وأدركت أنه من الصعب الإجابة على سؤالهم دون التطرق إلى حقيقة مفادها أن الناس لا يمكنهم حتى الاتفاق
على ا هو الجنس_ في المقام الأول.

View File

@ -0,0 +1,78 @@
# تلك الضمائر المخيفة
<small>2021-08-22 | [@andrea](/@andrea)، مُترجَمة من قِبَل الفريق العربي.</small>
![سلفيا ديزيوبا على ميديوم: المطالبة بالضمائر. لا، لا أريد أن أخبرك بضمائري، ولا أريد أن أعرف ضمائرك.](/img/en/blog/demanding-pronouns.png)
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/those-scary-pronouns" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
لقد صادفت مقالًا على ميديوم بعنوان [المطالبة بالضمائر](https://web.archive.org/web/20220520175246/https://medium.com/the-venting-machine/demanding-pronouns-b819ab23f5df).
وبينما عادةً ما لن أزعج نفسي للرد على مثل هذه التفاهات الصريحة ضد غير الثنائيين،
إلا أن هذا المقال كان سخيفًا لدرجة أنني يجب أن أختار أجزاءه "المفضلة" وأشاركها مع تعليق، حتى وإن كان تعليقًا مختصرًا.
وأول جزء مروع هو الفقرة الأولى حرفيًا:
> يبدو أنه في كل مكان ألتفت فيه، يُطلب مني الحديث عن ضمائري. جوابي؟
> فقط خاطبني بأي طريقة تجعلك تشعر بالراحة،
> ولكن لا تجعلني أمر بتعذيب إخبارك بـ "هي/لها" في كل تفاعل.
> أنا فتاة؛ أبدو كفتاة — أنا أنثى بيولوجية. ماذا تريد مني أكثر من ذلك؟
يمكنني أن أتحدث لفترة طويلة عن أهمية جعل مشاركة الضمائر شيء عادي
وكيف أن الاستماع لاحتياجات المتحولين هو تعبير عن التعاطف الأساسي، وكيف أن ليس كل من "يبدو كفتاة"
هو بالضرورة "أنثى بيولوجية"، أو كيف أن الضمائر تتعلق بالقواعد اللغوية، وليس البيولوجيا،
لذلك، لا يهمني أعضاؤها التناسلية عندما أتساءل ببساطة عن كيفية الإشارة إليها.
ولكن هذا سيشتت الانتباه عن الكلمة الأكثر أهمية في هذه الفقرة:
> تعذيب
نعم، هي تسميه "تعذيب" عندما يسألها شخص... سؤالًا. وليس سؤالًا شخصيًا جدًا أو معقدًا.
إنها لا تُسأل عن أعضائها التناسلية. إنها لا تُسأل عن هويتها الجنسية.
شخص ما ببساطة يحاول أن يحدد الأساس للحديث بمعرفة كيفية التحدث إليها / عنها.
والجواب هو حرفيًا مقطعان صوتيان!
نعم، قد يكون هذا السؤال _صعبًا_ في بعض الأحيان،
ولكنها ليست شخصًا متحولًا في الخزانة الذي قد يشعر بالقلق في محاولة تحديد ما إذا كانت الظروف آمنة بما يكفي
لتقديم ضمائرها الحقيقية لشخص ما. لا. إنها مجرد شخص أناني.
وهي ليست حتى تحاول أن تُبقي تطرفها معتدلًا. هي تبدأ مقالتها باستخدام
أقوى كلمة ممكنة يمكن استخدامها لوصف الوضع: "تعذيب".
أنتم تعرفون، ذلك الشيء المحظور بموجب العديد من الاتفاقيات الدولية والذي يُعتبر انتهاكًا لحقوق الإنسان.
أما بالنسبة لجزئي "المفضل" الآخر، فهو هذا:
> الآن بعد أن قمت بشكواي، أريد التحدث عن الصورة الأكبر هنا. أنا لست غاضبًا فقط.
> أكتب هذا لأقاتل من أجل البساطة. هذا العالم معقد بما فيه الكفاية.
> لماذا تحاول أن تضيف المزيد من الفوضى؟ شخص يريد أن يكون
> هم/له، آخر يريد هم/لهم، آخر هو/لهم، آخر هن/لهم — هل يجب أن أستمر؟
>
> لا يوجد احتمال في الجحيم أن أتذكر كل ذلك لكل شخص ألتقيه.
> يمكنك أن تنسى ذلك. نادني بالقديمة، قل لي أنني عالقة في طريقي. لا يهمني!
> فقط توقف عن المطالبة بشيء لا أريد القيام به، أو إلا أواجه غضبك.
> حسنًا، كن غاضبًا مني، لكن دعني في حالي!
أوه لا! تلك الصورة عن العالم تبدو مروعة بالفعل! كيف سننجو من أي تفاعل اجتماعي،
إذا كنا مجبرين على تذكر معلومات صغيرة عن الأشخاص الذين نتحدث إليهم؟!
حسنًا، بخلاف الأسماء، بالطبع. سؤال شخص عن اسمه (ومحاولة عدم نسيانه) هو مجرد أدب أساسي.
وهو مفيد جدًا، لأنه يتيح لك معرفة كيفية مناداة الشخص.
لكن الأسماء تختلف تمامًا عن الضمائر!
على سبيل المثال، تحتاج إلى معرفة _اسم_ شخص لتكوين جملة مثل "كتبت _سلفيا_ مقالًا معاديًا للمتحولين..."،
لكن تحتاج إلى معرفة _ضمائر_ شخص لتكوين جملة مثل "...وأنا أكره تمامًا _مقالها_ المعادي للمتحولين".
أفهم؟ شيء مختلف تمامًا.
هل يمكنك أن تتخيل مثل هذا العالم الرهيب، عالم حيث تلتقي بأشخاص، وهم _يعذبونك_ بأسئلة مثل
"ما اسمك؟"، وحتى يخبرونك بأسمائهم، وحتى يتوقعون منك أن تتذكره؟
أعرف! لا يطاق تمامًا!
حسنًا، سلفيا تعيش في عالم كهذا. عالم حيث لدى الناس الجرأة في أن يسألوا ماذا تريد أن يُطلق عليها.
وعندما أرى _معاناتها الرهيبة_ لا يسعني إلا أن أقول شيئًا واحدًا:
<iframe src="https://giphy.com/embed/J8FZIm9VoBU6Q" width="480" height="332" frameBorder="0" class="giphy-embed" allowFullScreen style="max-width: 100%"></iframe>
<p><a href="https://giphy.com/gifs/reaction-community-good-J8FZIm9VoBU6Q">بواسطة GIPHY</a></p>

View File

@ -4,6 +4,15 @@
![لقطة شاشة لرسالة الحظر مع النص: الروابط المراد إزالتها: pronouns.page. نقوم بإزالة الروابط التي تحتوي على: - البريد العشوائي؛ دعم أو الإشادة بالإرهاب أو الجريمة المنظمة أو جماعات الكراهية. - التماس الخدمات الجنسية؛ - بيع الأسلحة النارية والمخدرات](/img/ar/blog/instagram-ban.png)
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/meta-ban" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
في يوم الأحد 26 مارس، علمنا أن إنستجرام وفيسبوك بدأتا بحظر روابط صفحتنا ومطالبة المستخدمين بحذفها من ملفاتهم الشخصية.
نحن حزينون ومنزعجون من هذا الوضع. لا علاقة لبوابتنا بالرسائل غير المرغوب فيها، الإرهاب، الجرائم، جماعات الكراهية، الخدمات الجنسية، الأسلحة النارية، أو المخدرات.

View File

@ -4,6 +4,15 @@
![شعار الخادم (شعارنا على خلفية قوس قزح باستيل) بجوار شعار Discord. النص أدناه: discord.pronons.page](/img/ar/blog/community-discord.png)
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/community-discord" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
نحن متحمسون للإعلان عن خادم الـDiscord المجتمعي الخاص بنا!
نريده أن يكون ملاذًا آمنًا لأعضاء مجتمع المثليين حيث يمكن للجميع التعبير عن أنفسهم بحرية،
والشعور بالتفهم والقبول.

View File

@ -1,6 +1,15 @@
# شعارنا الجديد
<small>2022-11-19 | الجماعية، مُترجَمة من قِبَل [@WoofWoofer](/@WoofWoofer). </small>
<small>2022-01-28 | الجماعية، مُترجَمة من قِبَل [@WoofWoofer](/@WoofWoofer). </small>
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/new-logo" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
بدأ موقعنا كمشروع بسيط وصغير وكان شعاره يعكس هذه الحقيقة: فقد كان مجرد أيقونة عامة للعلامات (تمثل الضمائر والتصنيفات). لكن مع المزيد من الميزات، و الحركة، والمستخدمين. وبسبب زيادة أعضاء الفريق، جاءت احتياجات جديدة، وأفكار جديدة، والمهارات والمواهب اللازمة لتحقيقها.

View File

@ -5,6 +5,15 @@
![علمان يلوحان في مهب الريح على خلفية سماء زرقاء: فخر التقدم والفوضى المثلية](/img/ar/blog/queer-lgbtq.png)
<p style="margin-top: -.75rem"><small class="text-muted">تأثير التلويح بالعلم مصنوع من خلال <a href="https://krikienoid.github.io/flagwaver/" target="_blank" rel="noopener">Flagwaver</a>. صورة الخلفية بواسطة <a href="https://unsplash.com/@sklepacki">Stephanie Klepacki</a> على <a href="https://unsplash.com/photos/white-clouds-and-blue-sky-gXG_2TpSBOch" target="_blank" rel="noopener">Unsplash</a></small></p>
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/queer-lgbtq" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
لقد أدركت أنه قد مر وقت طويل منذ أن وصفت نفسي "عضوًا في مجتمع المثلية" (LGBTQIA+) وبدلاً من ذلك، أصبحت أطلق على نفسي "شاذ (كوير)" فقط.
وبالطبع، جزء من السبب هو سهولة نطق الكلمة بالمقارنة؛
وجزء آخر من السبب هو المعارك المستمرة حول أي الحروف يجب تضمينها في الاختصار وأيها لا.

View File

@ -2,6 +2,15 @@
<small>2022-09-04 | [@andrea](/@andrea)، مُترجَمة من قِبَل الفريق العربي.</small>
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/why-ads" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
لا أحب الإعلانات، ولا أعتقد أن أحداً يحبها... لذا لم تكن قرارات سهلة حقاً بالنسبة لنا...
لكن في الآونة الأخيرة، قررنا أن نبدأ تجربة حيث نمول المشروع باستخدام الإعلانات بدلاً من التبرعات.
في روح الشفافية التي حاولت دائماً الحفاظ عليها هنا، أود أن أخبركم أكثر عن تلك القرارات.

View File

@ -4,6 +4,18 @@
<img src="/img/ar/logo/logo-full.png" class="hero invertible my-4" alt="شِعار الجماعية">
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://zaimki.pl/blog/sk%C4%85d-nazwa-kolektywu" target="_blank" rel="noopener" class="badge text-bg-info">Polski</a>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/why-the-name" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
بالنسبة للناطقين باللغة البولندية، يُعد اسم مجموعتنا إشارة واضحة ولكن ليس بالضرورة للجميع. لذا إليكم تفسيرًا قصيرًا إذا كنتم مهتمين:
_(النسخة البولندية، الأكثر تفصيلاً، متاحة [هنا](https://zaimki.pl/blog/sk%C4%85d-nazwa-kolektywu))_

View File

@ -4,6 +4,15 @@
![لقطة مقربة لعين مغلقة مع ظلال عيون بألوان علم المتحولين جنسيًا (https://unsplash.com/photos/a0KL1Um0wBA)](/img/ar/blog/trans-eyeshadow.png)
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/not-just-pronouns" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
هل رأيت كل تلك التغريدات الغبية التي يمكن فضحها بسهولة حول الضمائر مؤخرًا؟
يبدو الأمر مثل "لم يستخدم يسوع الضمائر أبدًا!" أو "لا ضمائر في إعلان الاستقلال!"...
إنهم <i>مخطئون من الناحية الفنية</i>، مع الأخذ في الاعتبار أن الضمائر ليست سوى جزء من الكلام الذي يستخدمه الجميع في كل جملة تقريبًا

View File

@ -6,6 +6,15 @@
<p style="margin-top: -.75rem"><small class="text-muted">تصوير <a href="https://unsplash.com/fr/@gmalhotra?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">غياتري مالهوترا</a> على
<a href="https://unsplash.com/photos/ft3ndjrS2TI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></small></p>
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/does-gender-exist" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
</ul>
أثناء تصفح الردود في [تعداد غير الثنائيين البولندي لعام 2023](https://zaimki.pl/spis)، صادفت ردًا حيث رفض المستجيب الإجابة على معظم الأسئلة، مضيفًا ملاحظات مثل "الجنس لا يوجد"، "الجنس لا يهم"، "لا نحتاج إلى ضمائر"، "لا يمكنك الهروب من التصنيفات بإضافة المزيد من التصنيفات"، "كن نفسك"، "تخلى عن مفهوم الجنس"، "لا تدع نفسك تتقيد"، إلخ. هذه ليست صوتًا وحيدًا (مع أنه نادر جدًا) لقد ناقشت قضايا مشابهة مع شخص لا يحمل جنسًا يعمل حاليًا على إحدى نسخ اللغات القادمة من صفتحتنا؛ وأنا نفسي لا أحمل جنسًا وقد خضت في تأملات مشابهة عدة مرات.
لـ[مجلة "Poczytałosie"](https://zaimki.pl/zin)، كتبت قطعة ([“Analogies”](https://avris.it/texts/analogie))، حيث أتناول هذا الموضوع كيف من منظور شخص لا يشعر بالاتصال بأي جنس ويفهمه أكثر من الناحية الفكرية بدلاً من التجربة الشخصية، يبدو تقسيم الناس إلى فئات مرتبطة إلى حد ما بأعضائهم التناسلية أمرًا سخيفًا كما لو كنا نتعامل مع بعضنا البعض بشكل مختلف بناءً على فصيلة دمنا أو لون شعرنا. من الصعب بالنسبة لي رؤية الجنس كأي شيء آخر غير تقسيم تعسفي وضار.

View File

@ -936,6 +936,22 @@ profile:
action: 'Entferne dich selber'
confirm: 'Bist du sicher, dass du dein Profil von dem Umfeld von @%username% entfernen möchtest?'
add: 'Personen zu deinem Umfeld hinzufügen'
blocks:
header: 'Blockierte Accounts'
action: 'Blockiere diese Person'
confirm: >
Bist du sicher, dass du @%username% blockieren möchtest?
Ihr werdet nicht mehr gegenseitig eure Profile sehen
oder euch gegenseitig zu eurem Umfeld hinzufügen können.
Die Blockierung kann später in deiner Accountseite aufgehoben werden.
unblock:
action: 'Blockierung aufheben'
confirm: >
Bist du sicher, dass du @%username% nicht mehr blockieren möchtest?
Ihr werdet wieder gegenseitig eure Profile ansehen
und euch gegenseitig zu eurem Umfeld hinzufügen können.
empty: 'Du hast derzeit keine Personen blockiert.'
instruction: 'Du kannst Personen blockieren, wenn du deren Profil besuchst.'
timezone:
header: 'Zeitzone'
placeholder: 'Wähle deine Zeitzone'

View File

@ -4,7 +4,7 @@
![Sylvia Dziuba on Medium: Demanding Pronouns. No, I dont want to give you mine, and I dont want to know yours.](/img/en/blog/demanding-pronouns.png)
So I stumbled upon an article on Medium titled [“Demanding Pronouns”](https://medium.com/the-venting-machine/demanding-pronouns-b819ab23f5df).
So I stumbled upon an article on Medium titled [“Demanding Pronouns”](https://web.archive.org/web/20220520175246/https://medium.com/the-venting-machine/demanding-pronouns-b819ab23f5df).
And while I normally wouldn't even bother to dignify such blatantly enbyphobic bullshit with a reply,
this one is just so over the top asinine that I just have to pick my “favourite” parts and share them with a comment, even a brief one.

View File

@ -92,10 +92,10 @@ export default [
new Event('{/terminology#two%20spirit=Two Spirit} Awareness Day (US)', 'Two Spirit_', 7, day(11), EventLevel.Day, ['two spirit']),
new Event('LGBT Center Awareness Day (US)', null, 10, day(19), EventLevel.Day),
new Event('{https://twitter.com/_EQUALGROUND_/status/1440232964286124050=Lesbian Visibility Day} (Sri Lanka)', 'Lesbian', 9, day(21), EventLevel.Day),
new Event('{https://www.cdc.gov/hiv/library/awareness/nlaad.html=Latinx AIDS Awareness Day} (US)', '_red-ribbon', 10, day(15), EventLevel.Day, ['aids']),
new Event('{https://www.cdc.gov/hiv/library/awareness/shaad.html=Southern HIV/AIDS Awareness Day} (US)', '_red-ribbon', 8, day(20), EventLevel.Day, ['aids']),
new Event('{https://www.cdc.gov/hiv/library/awareness/napihaad.html=Asian and Pacific Islander HIV/AIDS Awareness Day} (US)', '_red-ribbon', 5, day(19), EventLevel.Day, ['aids']),
new Event('{https://www.cdc.gov/hiv/library/awareness/nnhaad.html=Native HIV/AIDS Awareness Day} (US)', '_red-ribbon', 3, day(20), EventLevel.Day, ['aids']),
new Event('{https://www.hiv.gov/events/awareness-days/latino=Latinx AIDS Awareness Day} (US)', '_red-ribbon', 10, day(15), EventLevel.Day, ['aids']),
new Event('{https://www.hiv.gov/events/awareness-days/southern-hiv-aids-awareness-day=Southern HIV/AIDS Awareness Day} (US)', '_red-ribbon', 8, day(20), EventLevel.Day, ['aids']),
new Event('{https://www.hiv.gov/events/awareness-days/asian-pacific-islander=Asian and Pacific Islander HIV/AIDS Awareness Day} (US)', '_red-ribbon', 5, day(19), EventLevel.Day, ['aids']),
new Event('{https://www.hiv.gov/events/awareness-days/native=Native HIV/AIDS Awareness Day} (US)', '_red-ribbon', 3, day(20), EventLevel.Day, ['aids']),
new Event('{/terminology#transgender=Trans} Visibility Day (Brazil)', 'Transgender', 1, day(29), EventLevel.Day, ['transgender']),
new Event('National {/terminology#pride=Gay Pride} Day (Brazil)', 'LGBTQ', 3, day(25), EventLevel.Day),
new Event('{/terminology#lesbian=Lesbian} Visibility Day (Brazil)', 'Lesbian', 8, day(29), EventLevel.Day, ['lesbian']),

View File

@ -1067,6 +1067,20 @@ profile:
action: 'Remove yourself'
confirm: 'Are you sure you want to remove your profile from @%username%''s circle?'
add: 'Add people to your circle'
blocks:
header: 'Blocked accounts'
action: 'Block this person'
confirm: >
Are you sure you want to block @%username%?
You will not be able to see each other's cards or add each other to your circles.
This can be reversed in your Account page.
unblock:
action: 'Unblock this person'
confirm: >
Are you sure you want to unblock @%username%?
You will be able to see each other's cards or add each other to your circles.
empty: 'You currently do not have any accounts blocked.'
instruction: 'You can block a person by visiting their profile.'
sensitive:
header: 'Content warning'
info: >
@ -1997,12 +2011,12 @@ calendar:
unlabeled_day: '{https://unlabeledidentity.carrd.co/=Unlabeled Visibility Day}'
trans_youth_day: '{https://elmarplatense.com/2021/05/14/se-conmemoro-el-primer-dia-de-la-visibilidad-de-las-nineces-y-adolescencias-trans-en-general-pueyrredon/=Trans Children and Youth Visibility Day}'
hiv_long_term_survivors_day: '{https://www.hiv.gov/events/awareness-days/hiv-long-term-survivors-day=HIV Long-Term Survivors Awareness Day}'
women_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nwghaad.html=Women and Girls HIV/AIDS Awareness Day}'
youth_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nyhaad.html=Youth HIV/AIDS Awareness Day}'
gay_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/ngmhaad.html=Gay Men''s HIV/AIDS Awareness Day}'
black_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nbhaad.html=Black HIV/AIDS Awareness Day}'
hiv_aging_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nhaad.html=HIV/AIDS and Aging Awareness Day}'
trans_hiv_testing_day: '{https://www.cdc.gov/hiv/library/awareness/nthtd.html=Transgender HIV Testing Day}'
women_hiv_awareness_day: '{https://www.hiv.gov/events/awareness-days/women-and-girls=Women and Girls HIV/AIDS Awareness Day}'
youth_hiv_awareness_day: '{https://www.hiv.gov/events/awareness-days/youth=Youth HIV/AIDS Awareness Day}'
gay_hiv_awareness_day: '{https://www.hiv.gov/events/awareness-days/gay-mens=Gay Men''s HIV/AIDS Awareness Day}'
black_hiv_awareness_day: '{https://www.hiv.gov/events/awareness-days/black=Black HIV/AIDS Awareness Day}'
hiv_aging_awareness_day: '{https://www.hiv.gov/events/awareness-days/aging=HIV/AIDS and Aging Awareness Day}'
trans_hiv_testing_day: '{https://clinicalinfo.hiv.gov/en/news/national-transgender-hiv-testing-day-resources=Transgender HIV Testing Day}'
mspec_lesbian_day: '{https://twitter.com/MspecLesbianss=Mspec Lesbian Visibility & Awareness Day}'
mspec_lesbian_week: '{https://twitter.com/MspecLesbianss=Mspec Lesbian Visibility & Awareness Week}'
mspec_gay_day: 'Mspec Gay Visibility & Awareness Day'

View File

@ -608,6 +608,10 @@ user:
Unukonte vi povas havi nur unu karton po lingvo kaj tiuj kartoj estas ligitaj per la komuna @uzantnomo. Tamen
eble vi volus krei ankaŭ multajn sendependajn kontojn, ekzemple unu por laboro kaj alian apartan por via
amikaro. Se vi aldonos la kontojn ĉi tien, vi povos rapide ŝanĝadi inter ili.
emailMissing: >
Via konto estis kreita per metodo, kiu ne kunhavigis kun ni konfirmitan retpoŝtan adreson. Bonvolu ŝanĝi la
ĉi-suban lokokupan tekston al via adreso. Tiel vi havos retropaŝan ensalutmetodon, se vi perdos aliron al tiu,
kiun vi uzis.
tooFewAuthMethods: 'Ni rekomendas konekti almenaŭ du metodojn de aŭtentigo, por ke vi havu iun rezervan.'
profile:
@ -660,6 +664,10 @@ profile:
linksRecommended: 'Ni rekomendas ligi al'
verifiedLinks:
header: 'Aprobitaj ligiloj'
info: >
Ligiloj metitaj ĉe profiloj estos markitaj per ŝildeto, se ili estas konfirmitaj per ensalutado per taŭga
aŭtentiga metodo, aŭ per la etikego <code>rel="me"</code> direktiganta al la karto. Ankaŭ niaj ligiloj
inkluzivas la tagon <code>rel="me"</code>, do ankaŭ aliaj retejoj povas konfirmi vian karton ĉe sia flanko.
column: 'Kolumno'
opinions:
header: 'Klarigo/opinisignoj'

View File

@ -4,6 +4,18 @@
![Capturas de pantalla de títulos de las publicaciones en reddit (en inglés), todas citadas abajo](/img/es/blog/can-i-be.png)
<ul class="list-inline">
<li class="list-inline-item">
<span class="fal fa-language"></span>
</li>
<li class="list-inline-item">
<a href="https://en.pronouns.page/blog/can-i-be" target="_blank" rel="noopener" class="badge text-bg-info">English</a>
</li>
<li class="list-inline-item">
<a href="https://ar.pronouns.page/المدونة/أيمكنني-أن-أكون" target="_blank" rel="noopener" class="badge text-bg-info">العربية (الفصحى)</a>
</li>
</ul>
Recientemente he notado que muchas publicaciones en algunos subreddits que sigo son básicamente peticiones de validación a la identidad propia.
Aquí unos pocos ejemplos (traducidos) de sólo uno de ellos ([r/Nonbinary](https://reddit.com/r/Nonbinary)):

37
locale/generate.ts Normal file
View File

@ -0,0 +1,37 @@
import fs from 'fs/promises';
import { createGenerator } from 'ts-json-schema-generator';
import type { Config } from '~/locale/config.ts';
import localeDescriptions from '~/locale/locales.ts';
import { loadSuml } from '~/server/loader.ts';
const __dirname = new URL('.', import.meta.url).pathname;
const generateConfigJsonSchema = async () => {
const schema = createGenerator({
path: `${__dirname}/config.ts`,
strictTuples: true,
markdownDescription: true,
// speed up schema generation; type checking happens separately
skipTypeCheck: true,
}).createSchema('Config');
await fs.writeFile(`${__dirname}/config.schema.json`, `${JSON.stringify(schema, null, 4)}\n`);
};
const generateFontsModule = async () => {
const configByLocale = Object.fromEntries(await Promise.all(localeDescriptions.map(async (locale) => {
return [locale.code, await loadSuml<Config>(`locale/${locale.code}/config.suml`)] as const;
})));
const fontHeadingsByLocale = Object.fromEntries(Object.entries(configByLocale)
.map(([localeCode, config]) => [localeCode, config.style.fontHeadings]));
await fs.writeFile(
`${__dirname}/fonts.ts`,
`export const fontHeadingsByLocale: Record<string, string[]> = ${JSON.stringify(fontHeadingsByLocale)}`,
);
};
await Promise.all([generateConfigJsonSchema(), generateFontsModule()]);

View File

@ -1,15 +0,0 @@
import fs from 'fs';
import { createGenerator } from 'ts-json-schema-generator';
const __dirname = new URL('.', import.meta.url).pathname;
const schema = createGenerator({
path: `${__dirname}/config.ts`,
strictTuples: true,
markdownDescription: true,
// speed up schema generation; type checking happens separately
skipTypeCheck: true,
}).createSchema('Config');
fs.writeFileSync(`${__dirname}/config.schema.json`, `${JSON.stringify(schema, null, 4)}\n`);

View File

@ -2051,12 +2051,12 @@ calendar:
unlabeled_day: '{https://unlabeledidentity.carrd.co/=Umerkas}-synlighetsdagen'
trans_youth_day: '{https://elmarplatense.com/2021/05/14/se-conmemoro-el-primer-dia-de-la-visibilidad-de-las-nineces-y-adolescencias-trans-en-general-pueyrredon/=Trans-barns og -ungdommers synlighetsdag}'
hiv_long_term_survivors_day: '{https://www.hiv.gov/events/awareness-days/hiv-long-term-survivors-day=Bevissthetsdagen for langtidsoverlevere av HIV}'
women_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nwghaad.html=Bevissthetsdagen om HIV/AIDS for kvinner og jenter}'
youth_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nyhaad.html=Bevissthetsdagen om HIV/AIDS for ungdom}'
gay_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/ngmhaad.html=Bevissthetsdagen om HIV/AIDS for homofile menn}'
black_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nbhaad.html=Bevissthetsdagen om HIV/AIDS for svarte}'
hiv_aging_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nhaad.html=Bevissthetsdagen om HIV/AIDS og aldring}'
trans_hiv_testing_day: '{https://www.cdc.gov/hiv/library/awareness/nthtd.html=Transpersoners HIV-testdag}'
women_hiv_awareness_day: 'Bevissthetsdagen om HIV/AIDS for kvinner og jenter'
youth_hiv_awareness_day: 'Bevissthetsdagen om HIV/AIDS for ungdom'
gay_hiv_awareness_day: 'Bevissthetsdagen om HIV/AIDS for homofile menn'
black_hiv_awareness_day: 'Bevissthetsdagen om HIV/AIDS for svarte'
hiv_aging_awareness_day: 'Bevissthetsdagen om HIV/AIDS og aldring'
trans_hiv_testing_day: 'Transpersoners HIV-testdag'
mspec_lesbian_day: '{https://twitter.com/MspecLesbianss=Den mspec-lesbiske synlighets- og bevissthetsdagen}'
mspec_lesbian_week: '{https://twitter.com/MspecLesbianss=Den mspec-lesbiske synlighets- og bevissthetsuke}'
mspec_gay_day: 'Den mspec-homofile synlighets- og bevissthetsdagen'

View File

@ -2053,12 +2053,12 @@ calendar:
unlabeled_day: '{https://unlabeledidentity.carrd.co/=Umerka}-synlegheitsdagen'
trans_youth_day: '{https://elmarplatense.com/2021/05/14/se-conmemoro-el-primer-dia-de-la-visibilidad-de-las-nineces-y-adolescencias-trans-en-general-pueyrredon/=Trans-barn og -ungdom sin synlegheitsdag}'
hiv_long_term_survivors_day: '{https://www.hiv.gov/events/awareness-days/hiv-long-term-survivors-day=Dagen for merksemd rundt HIV-langtidsoverlevarar}'
women_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nwghaad.html=Merksemdsdagen om HIV/AIDS for kvinner og jenter}'
youth_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nyhaad.html=Merksemdsdagen om HIV/AIDS for ungdom}'
gay_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/ngmhaad.html=Merksemdsdagen om HIV/AIDS for homofile menn}'
black_hiv_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nbhaad.html=Merksemdsdagen om HIV/AIDS for svarte}'
hiv_aging_awareness_day: '{https://www.cdc.gov/hiv/library/awareness/nhaad.html=Merksemdsdagen om HIV/AIDS og aldring}'
trans_hiv_testing_day: '{https://www.cdc.gov/hiv/library/awareness/nthtd.html=Transpersonar sin HIV-testdag}'
women_hiv_awareness_day: 'Merksemdsdagen om HIV/AIDS for kvinner og jenter'
youth_hiv_awareness_day: 'Merksemdsdagen om HIV/AIDS for ungdom'
gay_hiv_awareness_day: 'Merksemdsdagen om HIV/AIDS for homofile menn'
black_hiv_awareness_day: 'Merksemdsdagen om HIV/AIDS for svarte'
hiv_aging_awareness_day: 'Merksemdsdagen om HIV/AIDS og aldring'
trans_hiv_testing_day: 'Transpersonar sin HIV-testdag}'
mspec_lesbian_day: '{https://twitter.com/MspecLesbianss=Den mspec-lesbiske synlegheits- og merksemdsdagen}'
mspec_lesbian_week: '{https://twitter.com/MspecLesbianss=Den mspec-lesbiske synlegheits- og merksemdsveka}'
mspec_gay_day: 'Den mspec-homofile synlegheits- og merksemdsdagen'

View File

@ -117,7 +117,7 @@ Jeśli będzie chciało mi złożyć życzenia w Dzień Kobiet przyjmę je,
Nie zmienia to faktu, że moje święta przypadają 18 kwietnia to Dzień Niebinarnego Rodzica oraz 9 marca,
kiedy obchodzimy Polski Dzień Osób Niebinarnych.
<a href="/kalendarz/2023-03-09"><img src="/calendar/2023-03-09.png" alt="Kalendarz na marzec z zaznaczonymi queerowymi świętami i wylistowanymi tymi z 9-go marca"></a>
<a href="/kalendarz/2023-03-09"><img src="/calendar/pl/2023-03-09.png" alt="Kalendarz na marzec z zaznaczonymi queerowymi świętami i wylistowanymi tymi z 9-go marca"></a>
Jestem rodzicem, jestem osobą rodzicielską, jestem mamuś. Jestem sobą.

View File

@ -3290,7 +3290,7 @@ contact:
route: 'kolektyw-rjn'
workshops:
enabled: true
enabled: false
route: 'szkolenia'
email: 'szkolenia@zaimki.pl'

View File

@ -1690,6 +1690,20 @@ profile:
action: 'Usuń się'
confirm: 'Czy na pewno chcesz usunąć link do swojej wizytówki z kręgów osoby @%username%?'
add: 'Dodaj osoby do swojego kręgu'
blocks:
header: 'Zablokowane konta'
action: 'Zablokuj tę osobę'
confirm: >
Czy na pewno chcesz zablokować @%username%?
Nie będziecie mogły widzieć swoich wizytówek ani dodawać się nawzajem do kręgów.
Możesz cofnąć blokadę na stronie „Twoje konto”.
unblock:
action: 'Unblock this person'
confirm: >
Czy na pewno chcesz odblokować @%username%?
Będziecie mogły znowu widzieć swoje wizytówki oraz dodawać się nawzajem do kręgów.
empty: 'You currently do not have any accounts blocked.'
instruction: 'You can block a person by visiting their profile.'
sensitive:
header: 'Ostrzeżenie o zawartości'
info: >
@ -2119,7 +2133,7 @@ ban:
reason: 'Powód blokady'
visible: '(to będzie widoczne dla osoby zbanowanej)'
terms: 'Złamane punkty regulaminu (wymagane)'
action: 'Zablokuj tę osobę'
action: 'Zbanuj tę osobę'
confirm: 'Czy na pewno chcesz zbanować @%username%?'
header: 'Twoje konto jest zablokowane. Twoje profile nie są widoczne publicznie.'
banned: 'Konto nieaktywne'

View File

@ -654,7 +654,7 @@ profile:
defaults: 'Restaurar valores padrão'
propagate: 'Salvar alterações em seus cartões em todas as línguas'
timezone:
time: 'São %time% e é %weekday% dentro do meu fuso horário'
time: 'São %time% de um(a) %weekday% em meu fuso horário'
areas:
America: 'América'
Africa: 'África'
@ -868,7 +868,7 @@ footer:
visitDuration: 'Média da duração de visitas'
responseTime: 'Média do tempo de resposta'
cards: 'Cartões '
visitors: 'Visitantes de ocasião única'
visitors: 'Visitantes úniques'
pageviews: 'Visualizações de página'
realTimeVisitors: 'Atualmente online'
users: 'Usuáries registrades'

View File

@ -0,0 +1,7 @@
-- Up
UPDATE users
SET timesheets = REPLACE(timesheets, 'merch', 'monetisation')
WHERE timesheets is not null;
-- Down

View File

@ -0,0 +1,12 @@
-- Up
CREATE TABLE block_connections (
id TEXT NOT NULL PRIMARY KEY,
from_userId TEXT NOT NULL REFERENCES users ON DELETE CASCADE,
to_userId TEXT NOT NULL REFERENCES users ON DELETE CASCADE
);
CREATE INDEX "block_connections_from_userId" ON "block_connections" ("from_userId");
CREATE INDEX "block_connections_to_userId" ON "block_connections" ("to_userId");
-- Down

View File

@ -40,6 +40,10 @@ const colour = '#C71585';
const codes = ['_', ...localeDescriptions.map((localeDescription) => localeDescription.code)];
const configByLocale: Record<string, Config> = Object.fromEntries(await Promise.all(codes.map(async (localeCode) => {
return [localeCode, await loadSuml<Config>(`locale/${localeCode}/config.suml`)];
})));
let __dirname = new URL('.', import.meta.url).pathname;
if (process.platform === 'win32') {
// A small hack, for Windows can't have nice things.
@ -221,8 +225,7 @@ export default defineNuxtConfig({
hooks: {
async 'pages:extend'(routes) {
for (const code of codes) {
const config = await loadSuml<Config>(`locale/${code}/config.suml`);
for (const subroute of config.nouns.subroutes || []) {
for (const subroute of configByLocale[code].nouns.subroutes || []) {
routes.push({
path: `/${encodeURIComponent(subroute)}`,
name: `nouns-${code}-${subroute}`,

View File

@ -7,7 +7,7 @@
"./server/dotenv.ts"
],
"scripts": {
"prepare-dev": "nuxi prepare && pnpm run-file locale/generateSchemas.ts",
"prepare-dev": "nuxi prepare && pnpm run-file locale/generate.ts",
"dev": "nuxi dev",
"build": "NODE_OPTIONS=\"--max-old-space-size=5120\" nuxi build",
"run-file": "node --import=./server/ts-node-esm.js --env-file=.env",
@ -78,7 +78,7 @@
"@types/jsdom": "^21.1.7",
"@types/luxon": "^1.27.1",
"@types/markdown-it": "^14.0.1",
"@types/node": "^20.16.5",
"@types/node": "^22.13.13",
"@types/nodemailer": "^6.4.14",
"@types/papaparse": "^5.3.14",
"@types/speakeasy": "^2.0.10",
@ -86,16 +86,15 @@
"@vite-pwa/nuxt": "^0.10.6",
"@vitest/coverage-v8": "^3.0.8",
"@vue/test-utils": "^2.4.6",
"@vuepic/vue-datepicker": "^8.8.1",
"@vuepic/vue-datepicker": "^11.0.2",
"@vueuse/core": "^13.0.0",
"avris-columnist": "^0.3.4",
"avris-daemonise": "^0.0.2",
"avris-futurus": "^1.0.2",
"avris-generator": "^0.8.2",
"avris-sorter": "^0.0.3",
"avris-vue-share": "^1.0.1",
"bootstrap": "^5.3.1",
"chart.js": "3.7.0",
"chart.js": "4.4.8",
"clipboard": "^2.0.6",
"eslint": "^9.22.0",
"eslint-formatter-gitlab": "^5.1.0",
@ -121,7 +120,7 @@
"sass": "1.32.12",
"tree-kill": "^1.2.2",
"ts-json-schema-generator": "^1.5.0",
"typescript": "^5.7.2",
"typescript": "^5.8.3",
"vitest": "^3.0.8",
"vue-component-type-helpers": "^2.1.6",
"vue-tsc": "^2.2.8",

View File

@ -6,6 +6,7 @@ import type { LocalDescriptionWithConfig } from '~/server/admin.ts';
import buildLocaleList from '~/src/buildLocaleList.ts';
import { longtimeCookieSetting } from '~/src/cookieSettings.ts';
import { loadConfig } from '~/src/data.ts';
import { formatFonts } from '~/src/fonts.ts';
import { newDate } from '~/src/helpers.ts';
import { useMainStore } from '~/store/index.ts';
@ -200,7 +201,7 @@ const impersonate = async (email: string) => {
</div>
<template v-for="({ name, extra, config, published }, locale) in visibleLocales" :key="locale">
<h3>
<h3 :style="`--font-headings: ${formatFonts(config.style.fontHeadings)}`">
{{ name }} {{ extra ? `(${extra})` : '' }}
<small v-if="!published" class="text-muted">(not published yet)</small>
</h3>

View File

@ -210,7 +210,7 @@ const sumCells = (area: typeof areas[number] | undefined, month: keyof typeof mo
step="0.5"
class="form-control form-control-sm"
style="min-width: 3rem"
:disabled="dt(year, m) < closed || dt(year, m) > max"
:disabled="dt(year, +m) < closed || dt(year, +m) > max"
@focus="focusMonth = m;focusArea = areas.indexOf(area)"
@keydown="cellKeydown"
>

View File

@ -60,7 +60,7 @@ const username = computed(() => {
return usernameFromRoute;
}
if (userAsyncData.data.value.username !== usernameFromRoute && import.meta.client) {
if (userAsyncData.data.value.username && userAsyncData.data.value.username !== usernameFromRoute && import.meta.client) {
history.pushState(
'',
document.title,
@ -74,13 +74,13 @@ const username = computed(() => {
const { mainPronoun } = useMainPronoun(pronounLibrary, profile, translator);
const flags = buildFlags(config.locale);
useSimpleHead({
title: `@${username.value}`,
title: username.value ? `@${username.value}` : undefined,
description: computed(() => profile.value ? profile.value.description ?? null : null),
banner: `api/banner/@${username.value}.png`,
banner: username.value ? `api/banner/@${username.value}.png` : undefined,
noindex: true,
keywords: computed(() => profile.value && profile.value.flags
? profile.value.flags.map((flag) => translateForPronoun(flags[flag].display, mainPronoun.value))
: undefined),
keywords: computed(() => profile.value?.flags?.map((flagName) => flags[flagName])
.filter((flag) => flag !== undefined)
.map((flag) => translateForPronoun(flag.display, mainPronoun.value))),
}, translator);
await userAsyncData;

View File

@ -760,6 +760,10 @@ const propagateChanged = (field: string, checked: boolean): void => {
z-index: 5;
}
.dp__input_icon_pad {
padding-left: var(--dp-input-icon-padding);
}
.dp__input_focus {
box-shadow: $input-focus-box-shadow;
}

24
plugins/gtag.ts Normal file
View File

@ -0,0 +1,24 @@
export default defineNuxtPlugin(() => {
const config = useConfig();
if (!config.ads?.enabled || import.meta.env?.APP_ENV !== 'production') {
return;
}
useHead({
script: [
{
src: 'https://www.googletagmanager.com/gtag/js?id=G-TDJEP12Q3M',
async: true,
},
{
textContent: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-TDJEP12Q3M');
`,
type: 'text/javascript',
},
],
});
});

831
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@
cd "$(dirname "$0")" || exit
nvm_bin=~/.nvm/versions/node/"$(<.nvmrc)"/bin
nvm_bin=~/.nvm/versions/node/v"$(<.nvmrc)"/bin
export PATH=$nvm_bin:$PATH
if [ "$1" = "start" ]; then
exec node --env-file=.env .output/server/index.mjs -- "$(dirname "$0")" "$PORT"

View File

@ -247,7 +247,7 @@ router.get('/census/export', handleErrorAsync(async (req, res) => {
for (const question of config.census.questions!) {
if (question.type === 'checkbox') {
const answerForAggregate: Set<string> = new Set();
for (const [option, _comment] of question.options) {
for (const [option, _comment] of [...question.options, ...(question.optionsLast || [])]) {
const checked = (answers[i.toString()] || [] as string[]).includes(option);
answer[`${i}_${option}`] = checked ? 1 : '';
if (checked) {

View File

@ -495,14 +495,30 @@ const fetchCircles = async (
}));
};
const isUserBlocked = async (db: Database, user: any, otherUser: any): Promise<boolean> => {
if (!user || !otherUser || user.id === otherUser.id) {
return false;
}
const row = await db.get<{ c: number }>(SQL`
SELECT count(*) as c
FROM block_connections
WHERE (from_userId = ${user.id} AND to_userId = ${otherUser.id})
OR (from_userId = ${otherUser.id} AND to_userId = ${user.id})
`);
return row!.c > 0;
};
const router = Router();
const fetchProfilesRoute = async (req: Request, res: Response, locale: string, user: any): Promise<Response> => {
const isSelf = !!req.user && req.user.username === req.params.username;
const isAdmin = req.isGranted('users') || req.isGranted('community');
const opts = new ProfileOptions(req.query, req.locales);
const isBlocked = await isUserBlocked(req.db, req.user, user);
if (!user || user.bannedReason !== null && !isAdmin && !isSelf) {
if (!user || isBlocked || user.bannedReason !== null && !isAdmin && !isSelf) {
return res.json({
profiles: {},
});
@ -573,6 +589,11 @@ router.get('/profile/get-id/:id', handleErrorAsync(async (req, res) => {
}));
router.get('/profile/versions/:username', handleErrorAsync(async (req, res) => {
const user = await req.db.get<Pick<UserRow, 'id'>>(SQL`SELECT * FROM users WHERE usernameNorm = ${normalise(req.params.username)}`);
if (await isUserBlocked(req.db, req.user, user)) {
return res.json([]);
}
return res.json((await req.db.all<Pick<ProfileRow, 'locale'>>(SQL`
SELECT
profiles.locale
@ -832,7 +853,7 @@ router.post('/profile/save', handleErrorAsync(async (req, res) => {
for (const connection of profile.circle) {
const toUserId = usernameIdMap[normaliseWithLink(connection.username)];
const relationship = connection.relationship.substring(0, 64).trim();
if (toUserId === undefined || !relationship) {
if (toUserId === undefined || !relationship || await isUserBlocked(req.db, req.user, { id: toUserId })) {
continue;
}
await req.db.get(SQL`INSERT INTO user_connections (id, from_profileId, to_userId, relationship) VALUES (

View File

@ -1006,4 +1006,57 @@ router.get('/user/social-lookup/:provider/:identifier', handleErrorAsync(async (
return res.json(row ? row.username : null);
}));
router.get('/user/blocks', handleErrorAsync(async (req, res) => {
if (!req.user) {
return res.status(401).json({ error: 'Unauthorised' });
}
const rows = await req.db.all<{ id: string; to_userId: string; to_username: string }[]>(SQL`
SELECT b.id, b.to_userId, u.username AS to_username
FROM block_connections b
LEFT JOIN users u ON b.to_userId = u.id
WHERE from_userId = ${req.user.id}
ORDER BY b.id DESC
`);
return res.json(rows);
}));
router.post('/user/block/:id', handleErrorAsync(async (req, res) => {
const id = req.params.id;
const blockedUser = await req.db.get<UserRow>(SQL`SELECT * FROM users WHERE id = ${id}`);
if (!req.user || !blockedUser || req.user.id === blockedUser.id) {
return res.status(401).json({ error: 'Unauthorised' });
}
await req.db.get(SQL`
INSERT INTO block_connections (id, from_userId, to_userId)
VALUES (${ulid()}, ${req.user.id}, ${blockedUser.id})
`);
await req.db.get(SQL`
DELETE FROM user_connections
WHERE (from_profileId IN (SELECT id FROM profiles WHERE userId = ${req.user.id}) AND to_userId = ${blockedUser.id})
OR (from_profileId IN (SELECT id FROM profiles WHERE userId = ${blockedUser.id}) AND to_userId = ${req.user.id})
`);
return res.json(null);
}));
router.post('/user/unblock/:id', handleErrorAsync(async (req, res) => {
const id = req.params.id;
const blockedUser = await req.db.get<UserRow>(SQL`SELECT * FROM users WHERE id = ${id}`);
if (!req.user || !blockedUser || req.user.id === blockedUser.id) {
return res.status(401).json({ error: 'Unauthorised' });
}
await req.db.get(SQL`
DELETE FROM block_connections
WHERE from_userId = ${req.user.id}
AND to_userId = ${blockedUser.id}
`);
return res.json(null);
}));
export default router;

View File

@ -21,7 +21,7 @@ export default async (prefix: string, url: string, ttlDays: number | null = null
return null;
}
const isSvg = url.toLowerCase().endsWith('.svg');
const isSvg = url.toLowerCase().replace(/\?.*$/, '').endsWith('.svg') || url.startsWith('data:image/svg+xml');
const key = `${prefix}/${md5(url)}.${isSvg ? 'svg' : 'png'}`;

View File

@ -11,8 +11,8 @@ interface Ad {
const SHOP_LINK = 'https://shop.pronouns.page';
const SHOP_DESC = 'Pronouns Page merch is here! Get yours at shop.pronouns.page or Etsy!';
const WORKSHOPS_PL_LINK = 'https://zaimki.pl/szkolenia';
const WORKSHOPS_PL_DESC = 'Oferujemy szkolenia i warsztaty dla firm i organizacji';
// const WORKSHOPS_PL_LINK = 'https://zaimki.pl/szkolenia';
// const WORKSHOPS_PL_DESC = 'Oferujemy szkolenia i warsztaty dla firm i organizacji';
const WORKSHOPS_EN_LINK = 'https://en.pronouns.page/workshops';
const WORKSHOPS_EN_DESC = 'We offer training and workshops for companies and organisations.';
const PHS_LINK = 'https://polishheritage.shop/?utm_source=pp&utm_medium=banner&utm_campaign=launch';
@ -37,24 +37,24 @@ export const adsInternal: Ad[] = [
display: 'd-block d-lg-none',
alt: SHOP_DESC,
},
{
enabled: true,
locale: 'pl',
image: 'workshops-pl.png',
placeholders: ['content-0'],
link: WORKSHOPS_PL_LINK,
display: 'd-none d-md-block',
alt: WORKSHOPS_PL_DESC,
},
{
enabled: true,
locale: 'pl',
image: 'workshops-pl-mobile.png',
placeholders: ['content-mobile-0'],
link: WORKSHOPS_PL_LINK,
display: 'd-block d-md-none',
alt: WORKSHOPS_PL_DESC,
},
// {
// enabled: true,
// locale: 'pl',
// image: 'workshops-pl.png',
// placeholders: ['content-0'],
// link: WORKSHOPS_PL_LINK,
// display: 'd-none d-md-block',
// alt: WORKSHOPS_PL_DESC,
// },
// {
// enabled: true,
// locale: 'pl',
// image: 'workshops-pl-mobile.png',
// placeholders: ['content-mobile-0'],
// link: WORKSHOPS_PL_LINK,
// display: 'd-block d-md-none',
// alt: WORKSHOPS_PL_DESC,
// },
{
enabled: false,
locale: 'en',

3
src/fonts.ts Normal file
View File

@ -0,0 +1,3 @@
export const formatFonts = (fonts: string[]): string => {
return fonts.map((font) => `'${font}'`).join(',');
};

View File

@ -37,7 +37,7 @@ export const AREAS = [
'blog',
'census',
'workshops',
'merch',
'monetisation',
'documentation',
'community',
'other',