Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2eb04adff | |||
| d4d5979a35 | |||
| dde6e54657 | |||
| 698a7513b8 |
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 1.0.1
|
||||||
|
|
||||||
|
* Fix relay images in nav
|
||||||
|
* Fix relay nav overflow
|
||||||
|
|
||||||
# 1.0.0
|
# 1.0.0
|
||||||
|
|
||||||
* Add alerts via Anchor
|
* Add alerts via Anchor
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ android {
|
|||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdk rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdk rootProject.ext.targetSdkVersion
|
||||||
versionCode 14
|
versionCode 15
|
||||||
versionName "1.0.0"
|
versionName "1.0.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ ext {
|
|||||||
compileSdkVersion = 35
|
compileSdkVersion = 35
|
||||||
targetSdkVersion = 35
|
targetSdkVersion = 35
|
||||||
androidxActivityVersion = '1.9.2'
|
androidxActivityVersion = '1.9.2'
|
||||||
androidxAppCompatVersion = '1.7.0'
|
//https://github.com/ionic-team/capacitor/issues/7866
|
||||||
|
// androidxAppCompatVersion = '1.7.0'
|
||||||
|
androidxAppCompatVersion = '1.6.1'
|
||||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||||
androidxCoreVersion = '1.15.0'
|
androidxCoreVersion = '1.15.0'
|
||||||
androidxFragmentVersion = '1.8.4'
|
androidxFragmentVersion = '1.8.4'
|
||||||
|
|||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Fetch tags and set to env vars
|
||||||
|
git fetch --prune --unshallow --tags
|
||||||
|
git describe --tags --abbrev=0
|
||||||
|
export VITE_BUILD_VERSION=$RENDER_GIT_COMMIT
|
||||||
|
export VITE_BUILD_HASH=$RENDER_GIT_COMMIT
|
||||||
|
|
||||||
|
# Remove link overrides
|
||||||
|
node remove-pnpm-overrides.js package.json
|
||||||
|
|
||||||
|
# When CI=true as it is on render.com, removing link overrides breaks the lockfile
|
||||||
|
pnpm i --no-frozen-lockfile
|
||||||
|
|
||||||
|
# Rebuild sharp
|
||||||
|
pnpm rebuild
|
||||||
|
|
||||||
|
# The build runs out of memory at times
|
||||||
|
NODE_OPTIONS=--max_old_space_size=16384 pnpm run build
|
||||||
@@ -351,14 +351,14 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -376,14 +376,14 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 1.0.0;
|
MARKETING_VERSION = 1.0.1;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// This script is necessary for installing stuff on a host, since our links don't exist there.
|
||||||
|
|
||||||
|
import fs from "fs"
|
||||||
|
|
||||||
|
const pkgName = process.argv[2]
|
||||||
|
|
||||||
|
if (!pkgName?.endsWith("package.json")) {
|
||||||
|
console.log("File passed was not a package.json file")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgName, "utf8"))
|
||||||
|
|
||||||
|
if (pkg.pnpm && pkg.pnpm.overrides) {
|
||||||
|
delete pkg.pnpm.overrides
|
||||||
|
fs.writeFileSync(pkgName, JSON.stringify(pkg, null, 2) + "\n")
|
||||||
|
console.log("Removed pnpm.overrides from package.json")
|
||||||
|
} else {
|
||||||
|
console.log("No pnpm.overrides found in package.json")
|
||||||
|
}
|
||||||
+2
-2
@@ -259,7 +259,7 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
|
|||||||
export const checkRelayAccess = async (url: string, claim = "") => {
|
export const checkRelayAccess = async (url: string, claim = "") => {
|
||||||
const socket = Pool.get().get(url)
|
const socket = Pool.get().get(url)
|
||||||
|
|
||||||
await socket.auth.attemptAuth(signer.get().sign)
|
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||||
|
|
||||||
const thunk = publishThunk({
|
const thunk = publishThunk({
|
||||||
event: createEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
|
event: createEvent(AUTH_JOIN, {tags: [["claim", claim]]}),
|
||||||
@@ -307,7 +307,7 @@ export const checkRelayAuth = async (url: string, timeout = 3000) => {
|
|||||||
const socket = Pool.get().get(url)
|
const socket = Pool.get().get(url)
|
||||||
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
|
const okStatuses = [AuthStatus.None, AuthStatus.Ok]
|
||||||
|
|
||||||
await socket.auth.attemptAuth(signer.get().sign)
|
await socket.auth.attemptAuth(e => signer.get()?.sign(e))
|
||||||
|
|
||||||
// Only raise an error if it's not a timeout.
|
// Only raise an error if it's not a timeout.
|
||||||
// If it is, odds are the problem is with our signer, not the relay
|
// If it is, odds are the problem is with our signer, not the relay
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
urls: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const {urls}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column menu gap-2">
|
||||||
|
{#each urls as url (url)}
|
||||||
|
<MenuSpacesItem {url} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
|
import {splitAt} from "@welshman/lib"
|
||||||
import {userProfile} from "@welshman/app"
|
import {userProfile} from "@welshman/app"
|
||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
@@ -8,6 +9,7 @@
|
|||||||
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
import SpaceAdd from "@app/components/SpaceAdd.svelte"
|
||||||
import ChatEnable from "@app/components/ChatEnable.svelte"
|
import ChatEnable from "@app/components/ChatEnable.svelte"
|
||||||
import MenuSpaces from "@app/components/MenuSpaces.svelte"
|
import MenuSpaces from "@app/components/MenuSpaces.svelte"
|
||||||
|
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
|
||||||
import MenuSettings from "@app/components/MenuSettings.svelte"
|
import MenuSettings from "@app/components/MenuSettings.svelte"
|
||||||
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
|
||||||
import {userRoomsByUrl, canDecrypt, PLATFORM_RELAY, PLATFORM_LOGO} from "@app/state"
|
import {userRoomsByUrl, canDecrypt, PLATFORM_RELAY, PLATFORM_LOGO} from "@app/state"
|
||||||
@@ -22,19 +24,33 @@
|
|||||||
|
|
||||||
const addSpace = () => pushModal(SpaceAdd)
|
const addSpace = () => pushModal(SpaceAdd)
|
||||||
|
|
||||||
const showSpacesMenu = () => (spacePaths.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd))
|
const showSpacesMenu = () => (spaceUrls.length > 0 ? pushModal(MenuSpaces) : pushModal(SpaceAdd))
|
||||||
|
|
||||||
|
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
|
||||||
|
|
||||||
const showSettingsMenu = () => pushModal(MenuSettings)
|
const showSettingsMenu = () => pushModal(MenuSettings)
|
||||||
|
|
||||||
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
const openChat = () => ($canDecrypt ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
|
||||||
|
|
||||||
|
const hasNotification = (url: string) => {
|
||||||
|
const path = makeSpacePath(url)
|
||||||
|
|
||||||
|
return !$page.url.pathname.startsWith(path) && $notifications.has(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
let windowHeight = $state(0)
|
||||||
|
|
||||||
|
const itemHeight = 56
|
||||||
|
const navPadding = 6 * itemHeight
|
||||||
|
const itemLimit = $derived((windowHeight - navPadding) / itemHeight)
|
||||||
const spaceUrls = $derived(Array.from($userRoomsByUrl.keys()))
|
const spaceUrls = $derived(Array.from($userRoomsByUrl.keys()))
|
||||||
const spacePaths = $derived(spaceUrls.map(url => makeSpacePath(url)))
|
const [primarySpaceUrls, secondarySpaceUrls] = $derived(splitAt(itemLimit, spaceUrls))
|
||||||
const anySpaceNotifications = $derived(
|
const anySpaceNotifications = $derived(spaceUrls.some(hasNotification))
|
||||||
spacePaths.some(path => !$page.url.pathname.startsWith(path) && $notifications.has(path)),
|
const otherSpaceNotifications = $derived(secondarySpaceUrls.some(hasNotification))
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window bind:innerHeight={windowHeight} />
|
||||||
|
|
||||||
<div class="sail sait saib relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
<div class="sail sait saib relative z-nav hidden w-14 flex-shrink-0 bg-base-200 pt-4 md:block">
|
||||||
<div class="flex h-full flex-col justify-between">
|
<div class="flex h-full flex-col justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -45,9 +61,18 @@
|
|||||||
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
<Avatar src={PLATFORM_LOGO} class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
{#each spaceUrls as url (url)}
|
{#each primarySpaceUrls as url (url)}
|
||||||
<PrimaryNavItemSpace {url} />
|
<PrimaryNavItemSpace {url} />
|
||||||
{/each}
|
{/each}
|
||||||
|
{#if secondarySpaceUrls.length > 0}
|
||||||
|
<PrimaryNavItem
|
||||||
|
title="Other Spaces"
|
||||||
|
class="tooltip-right"
|
||||||
|
onclick={showOtherSpacesMenu}
|
||||||
|
notification={otherSpaceNotifications}>
|
||||||
|
<Avatar icon="widget" class="!h-10 !w-10" />
|
||||||
|
</PrimaryNavItem>
|
||||||
|
{/if}
|
||||||
<PrimaryNavItem title="Add Space" onclick={addSpace} class="tooltip-right">
|
<PrimaryNavItem title="Add Space" onclick={addSpace} class="tooltip-right">
|
||||||
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
|
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
|
||||||
</PrimaryNavItem>
|
</PrimaryNavItem>
|
||||||
|
|||||||
+24
-19
@@ -11,26 +11,29 @@ import {makeMentionNodeView} from "./MentionNodeView"
|
|||||||
import ProfileSuggestion from "./ProfileSuggestion.svelte"
|
import ProfileSuggestion from "./ProfileSuggestion.svelte"
|
||||||
|
|
||||||
export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
|
export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
|
||||||
try {
|
const $signer = signer.get()
|
||||||
const event = await signer.get()!.sign(
|
const headers: Record<string, string> = {
|
||||||
makeEvent(BLOSSOM_AUTH, {
|
"X-Content-Type": "text/plain",
|
||||||
tags: [
|
"X-Content-Length": "1",
|
||||||
["t", "upload"],
|
"X-SHA-256": "73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac",
|
||||||
["server", url],
|
}
|
||||||
["expiration", String(now() + 30)],
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const res = await fetch(normalizeUrl(url) + "/upload", {
|
try {
|
||||||
method: "head",
|
if ($signer) {
|
||||||
headers: {
|
const event = await signer.get().sign(
|
||||||
Authorization: `Nostr ${btoa(JSON.stringify(event))}`,
|
makeEvent(BLOSSOM_AUTH, {
|
||||||
"X-Content-Type": "text/plain",
|
tags: [
|
||||||
"X-Content-Length": "1",
|
["t", "upload"],
|
||||||
"X-SHA-256": "73cb3858a687a8494ca3323053016282f3dad39d42cf62ca4e79dda2aac7d9ac",
|
["server", url],
|
||||||
},
|
["expiration", String(now() + 30)],
|
||||||
})
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
headers.Authorization = `Nostr ${btoa(JSON.stringify(event))}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(normalizeUrl(url) + "/upload", {method: "head", headers})
|
||||||
|
|
||||||
return res.status === 200
|
return res.status === 200
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -38,6 +41,8 @@ export const hasBlossomSupport = simpleCache(async ([url]: [string]) => {
|
|||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getUploadUrl = async (spaceUrl?: string) => {
|
export const getUploadUrl = async (spaceUrl?: string) => {
|
||||||
|
|||||||
+2
-2
@@ -629,11 +629,11 @@ export const userRoomsByUrl = withGetter(
|
|||||||
const $userRoomsByUrl = new Map<string, Set<string>>()
|
const $userRoomsByUrl = new Map<string, Set<string>>()
|
||||||
|
|
||||||
for (const [_, room, url] of getGroupTags(tags)) {
|
for (const [_, room, url] of getGroupTags(tags)) {
|
||||||
addToMapKey($userRoomsByUrl, url, room)
|
addToMapKey($userRoomsByUrl, normalizeRelayUrl(url), room)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const url of getRelayTagValues(tags)) {
|
for (const url of getRelayTagValues(tags)) {
|
||||||
addToMapKey($userRoomsByUrl, url, GENERAL)
|
addToMapKey($userRoomsByUrl, normalizeRelayUrl(url), GENERAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
return $userRoomsByUrl
|
return $userRoomsByUrl
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M7.99986 12H8.00887M12.0044 12H12.0134M15.9908 12H15.9999" stroke="#1C274C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="12" cy="12" r="10" stroke="#1C274C" stroke-width="1.5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 323 B |
@@ -58,6 +58,7 @@
|
|||||||
import Mailbox from "@assets/icons/Mailbox.svg?dataurl"
|
import Mailbox from "@assets/icons/Mailbox.svg?dataurl"
|
||||||
import MapPoint from "@assets/icons/Map Point.svg?dataurl"
|
import MapPoint from "@assets/icons/Map Point.svg?dataurl"
|
||||||
import MenuDots from "@assets/icons/Menu Dots.svg?dataurl"
|
import MenuDots from "@assets/icons/Menu Dots.svg?dataurl"
|
||||||
|
import MenuDotsCircle from "@assets/icons/Menu Dots Circle.svg?dataurl"
|
||||||
import NotesMinimalistic from "@assets/icons/Notes Minimalistic.svg?dataurl"
|
import NotesMinimalistic from "@assets/icons/Notes Minimalistic.svg?dataurl"
|
||||||
import Pallete2 from "@assets/icons/Pallete 2.svg?dataurl"
|
import Pallete2 from "@assets/icons/Pallete 2.svg?dataurl"
|
||||||
import Paperclip from "@assets/icons/Paperclip.svg?dataurl"
|
import Paperclip from "@assets/icons/Paperclip.svg?dataurl"
|
||||||
@@ -149,6 +150,7 @@
|
|||||||
mailbox: Mailbox,
|
mailbox: Mailbox,
|
||||||
"map-point": MapPoint,
|
"map-point": MapPoint,
|
||||||
"menu-dots": MenuDots,
|
"menu-dots": MenuDots,
|
||||||
|
"menu-dots-circle": MenuDotsCircle,
|
||||||
"notes-minimalistic": NotesMinimalistic,
|
"notes-minimalistic": NotesMinimalistic,
|
||||||
"pallete-2": Pallete2,
|
"pallete-2": Pallete2,
|
||||||
paperclip: Paperclip,
|
paperclip: Paperclip,
|
||||||
|
|||||||
Reference in New Issue
Block a user