+# Lemmy v0.7.0 Release (2020-06-2X)
+
+## Breaking Change to our image server: Pictshare to Pict-rs migration guide
+
+This release replaces [pictshare](https://github.com/HaschekSolutions/pictshare) with [pict-rs](https://git.asonix.dog/asonix/pict-rs), and a script must be run on your server to upgrade.
+
+To update, run:
+
+```
+cd /lemmy
+wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/docker-compose.yml
+wget https://raw.githubusercontent.com/dessalines/lemmy/master/docker/prod/migrate-pictshare-to-pictrs.bash
+sudo bash migrate-pictshare-to-pictrs.bash
+```
+
+You'll also have to update your nginx config, use the [one here](https://github.com/LemmyNet/lemmy/blob/master/ansible/templates/nginx.conf).
+
+*You'll have to log in again to pick up your avatar*
+
+Apart from that, we've closed [~90 issues!](https://github.com/LemmyNet/lemmy/milestone/16?closed=1), including:
+
+- Site-wide list of recent comments.
+- Reconnecting websockets.
+- Lots more themes, including a default light one.
+- Expandable embeds for post links (and thumbnails), from iframely.
+- Better icons.
+- Emoji autocomplete to post and message bodies, and an Emoji Picker.
+- Post body now searchable.
+- Community title and description is now searchable.
+- Simplified cross-posts.
+- Better documentation.
+- LOTS more languages.
+- Lots of bugs squashed.
+
# Lemmy v0.6.0 Release (2020-01-16)
`v0.6.0` is here, and we've closed [41 issues!](https://github.com/LemmyNet/lemmy/milestone/15?closed=1)
[defaults]
inventory=inventory
+interpreter_python=/usr/bin/python3
[ssh_connection]
pipelining = True
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
- name: create lemmy folder
- file: path={{item.path}} state=directory
+ file: path={{item.path}} {{item.owner}} state=directory
with_items:
- - { path: '/lemmy/' }
- - { path: '/lemmy/volumes/' }
+ - { path: '/lemmy/', owner: 'root' }
+ - { path: '/lemmy/volumes/', owner: 'root' }
+ - { path: '/lemmy/volumes/pictrs/', owner: '991' }
- block:
- name: add template files
project_src: /lemmy/
state: present
pull: yes
+ remove_orphans: yes
- name: reload nginx with new config
shell: nginx -s reload
creates: '/etc/letsencrypt/live/{{domain}}/privkey.pem'
- name: create lemmy folder
- file: path={{item.path}} state=directory
+ file: path={{item.path}} owner={{item.owner}} state=directory
with_items:
- - { path: '/lemmy/' }
- - { path: '/lemmy/volumes/' }
+ - { path: '/lemmy/', owner: 'root' }
+ - { path: '/lemmy/volumes/', owner: 'root' }
+ - { path: '/lemmy/volumes/pictrs/', owner: '991' }
- block:
- name: add template files
project_src: /lemmy/
state: present
recreate: always
+ remove_orphans: yes
ignore_errors: yes
- name: reload nginx with new config
- ./lemmy.hjson:/config/config.hjson:ro
depends_on:
- postgres
- - pictshare
+ - pictrs
- iframely
postgres:
- ./volumes/postgres:/var/lib/postgresql/data
restart: always
- pictshare:
- image: hascheksolutions/pictshare:latest
+ pictrs:
+ image: asonix/pictrs:amd64-v0.1.0-r9
+ user: 991:991
ports:
- - "127.0.0.1:8537:80"
+ - "127.0.0.1:8537:8080"
volumes:
- - ./volumes/pictshare:/usr/share/nginx/html/data
+ - ./volumes/pictrs:/mnt
restart: always
iframely:
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block";
- # Upload limit for pictshare
- client_max_body_size 50M;
+ # Upload limit for pictrs
+ client_max_body_size 20M;
location / {
proxy_pass http://0.0.0.0:8536;
proxy_cache_min_uses 5;
}
- location /pictshare/ {
- proxy_pass http://0.0.0.0:8537/;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # Redirect pictshare images to pictrs
+ location ~ /pictshare/(.*)$ {
+ return 301 /pictrs/image/$1;
+ }
- if ($request_uri ~ \.(?:ico|gif|jpe?g|png|webp|bmp|mp4)$) {
- add_header Cache-Control "public, max-age=31536000, immutable";
- }
+ # pict-rs images
+ location /pictrs {
+ location /pictrs/image {
+ proxy_pass http://0.0.0.0:8537/image;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+ # Block the import
+ return 403;
}
location /iframely/ {
version: '3.3'
services:
- postgres:
- image: postgres:12-alpine
- environment:
- - POSTGRES_USER=lemmy
- - POSTGRES_PASSWORD=password
- - POSTGRES_DB=lemmy
- volumes:
- - ./volumes/postgres:/var/lib/postgresql/data
- restart: always
lemmy:
build:
volumes:
- ../lemmy.hjson:/config/config.hjson
depends_on:
+ - pictrs
- postgres
- - pictshare
- iframely
- pictshare:
- image: hascheksolutions/pictshare:latest
- ports:
- - "127.0.0.1:8537:80"
+ postgres:
+ image: postgres:12-alpine
+ environment:
+ - POSTGRES_USER=lemmy
+ - POSTGRES_PASSWORD=password
+ - POSTGRES_DB=lemmy
+ volumes:
+ - ./volumes/postgres:/var/lib/postgresql/data
+ restart: always
+
+ pictrs:
+ image: asonix/pictrs:v0.1.13-r0
+ ports:
+ - "127.0.0.1:8537:8080"
+ user: 991:991
volumes:
- - ./volumes/pictshare:/usr/share/nginx/html/data
+ - ./volumes/pictrs:/mnt
restart: always
iframely:
restart: always
lemmy:
- image: dessalines/lemmy:v0.6.74
+ image: dessalines/lemmy:v0.6.79
ports:
- "127.0.0.1:8536:8536"
restart: always
- ./lemmy.hjson:/config/config.hjson
depends_on:
- postgres
- - pictshare
+ - pictrs
- iframely
- pictshare:
- image: hascheksolutions/pictshare:latest
- ports:
- - "127.0.0.1:8537:80"
+ pictrs:
+ image: asonix/pictrs:v0.1.13-r0
+ ports:
+ - "127.0.0.1:8537:8080"
+ user: 991:991
volumes:
- - ./volumes/pictshare:/usr/share/nginx/html/data
+ - ./volumes/pictrs:/mnt
restart: always
- mem_limit: 100m
iframely:
image: dogbin/iframely:latest
--- /dev/null
+#!/bin/bash
+set -e
+
+if [[ $(id -u) != 0 ]]; then
+ echo "This migration needs to be run as root"
+ exit
+fi
+
+if [[ ! -f docker-compose.yml ]]; then
+ echo "No docker-compose.yml found in current directory. Is this the right folder?"
+ exit
+fi
+
+# Fixing pictrs permissions
+mkdir -p volumes/pictrs
+sudo chown -R 991:991 volumes/pictrs
+
+echo "Restarting docker-compose, making sure that pictrs is started and pictshare is removed"
+docker-compose up -d --remove-orphans
+
+if [[ -z $(docker-compose ps | grep pictrs) ]]; then
+ echo "Pict-rs is not running, make sure you update Lemmy first"
+ exit
+fi
+
+# echo "Stopping Lemmy so that users dont upload new images during the migration"
+# docker-compose stop lemmy
+
+pushd volumes/pictshare/
+echo "Importing pictshare images to pict-rs..."
+IMAGE_NAMES=*
+for image in $IMAGE_NAMES; do
+ IMAGE_PATH="$(pwd)/$image/$image"
+ if [[ ! -f $IMAGE_PATH ]]; then
+ continue
+ fi
+ echo -e "\nImporting $IMAGE_PATH"
+ ret=0
+ curl --silent --fail -F "images[]=@$IMAGE_PATH" http://127.0.0.1:8537/import || ret=$?
+ if [[ $ret != 0 ]]; then
+ echo "Error for $IMAGE_PATH : $ret"
+ fi
+done
+
+echo "Fixing permissions on pictshare folder"
+find . -type d -exec chmod 755 {} \;
+find . -type f -exec chmod 644 {} \;
+
+popd
+
+echo "Rewrite image links in Lemmy database"
+docker-compose exec -u postgres postgres psql -U lemmy -c "UPDATE user_ SET avatar = REPLACE(avatar, 'pictshare', 'pictrs/image') WHERE avatar is not null;"
+docker-compose exec -u postgres postgres psql -U lemmy -c "UPDATE post SET url = REPLACE(url, 'pictshare', 'pictrs/image') WHERE url is not null;"
+
+echo "Moving pictshare data folder to pictshare_backup"
+mv volumes/pictshare volumes/pictshare_backup
+
+echo "Migration done, starting Lemmy again"
+echo "If everything went well, you can delete ./volumes/pictshare_backup/"
+docker-compose start lemmy
use crate::db::user_view::*;
use crate::db::*;
use crate::{
- extract_usernames, fetch_iframely_and_pictshare_data, generate_random_string, naive_from_unix,
+ extract_usernames, fetch_iframely_and_pictrs_data, generate_random_string, naive_from_unix,
naive_now, remove_slurs, send_email, slur_check, slurs_vec_to_str,
};
return Err(APIError::err("site_ban").into());
}
- // Fetch Iframely and Pictshare cached image
- let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) =
- fetch_iframely_and_pictshare_data(data.url.to_owned());
+ // Fetch Iframely and pictrs cached image
+ let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
+ fetch_iframely_and_pictrs_data(data.url.to_owned());
let post_form = PostForm {
name: data.name.to_owned(),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
- thumbnail_url: pictshare_thumbnail,
+ thumbnail_url: pictrs_thumbnail,
};
let inserted_post = match Post::create(&conn, &post_form) {
return Err(APIError::err("site_ban").into());
}
- // Fetch Iframely and Pictshare cached image
- let (iframely_title, iframely_description, iframely_html, pictshare_thumbnail) =
- fetch_iframely_and_pictshare_data(data.url.to_owned());
+ // Fetch Iframely and Pictrs cached image
+ let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
+ fetch_iframely_and_pictrs_data(data.url.to_owned());
let post_form = PostForm {
name: data.name.to_owned(),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
- thumbnail_url: pictshare_thumbnail,
+ thumbnail_url: pictrs_thumbnail,
};
let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
Ok(res)
}
-#[derive(Deserialize, Debug)]
-pub struct PictshareResponse {
- status: String,
- url: String,
+#[derive(Deserialize, Debug, Clone)]
+pub struct PictrsResponse {
+ files: Vec<PictrsFile>,
+ msg: String,
+}
+
+#[derive(Deserialize, Debug, Clone)]
+pub struct PictrsFile {
+ file: String,
+ delete_token: String,
}
-pub fn fetch_pictshare(image_url: &str) -> Result<PictshareResponse, failure::Error> {
+pub fn fetch_pictrs(image_url: &str) -> Result<PictrsResponse, failure::Error> {
is_image_content_type(image_url)?;
let fetch_url = format!(
- "http://pictshare/api/geturl.php?url={}",
- utf8_percent_encode(image_url, NON_ALPHANUMERIC)
+ "http://pictrs:8080/image/download?url={}",
+ utf8_percent_encode(image_url, NON_ALPHANUMERIC) // TODO this might not be needed
);
let text = attohttpc::get(&fetch_url).send()?.text()?;
- let res: PictshareResponse = serde_json::from_str(&text)?;
- Ok(res)
+ let res: PictrsResponse = serde_json::from_str(&text)?;
+ if res.msg == "ok" {
+ Ok(res)
+ } else {
+ Err(format_err!("{}", &res.msg))
+ }
}
-fn fetch_iframely_and_pictshare_data(
+fn fetch_iframely_and_pictrs_data(
url: Option<String>,
) -> (
Option<String>,
}
};
- // Fetch pictshare thumbnail
- let pictshare_thumbnail = match iframely_thumbnail_url {
- Some(iframely_thumbnail_url) => match fetch_pictshare(&iframely_thumbnail_url) {
- Ok(res) => Some(res.url),
+ // Fetch pictrs thumbnail
+ let pictrs_thumbnail = match iframely_thumbnail_url {
+ Some(iframely_thumbnail_url) => match fetch_pictrs(&iframely_thumbnail_url) {
+ Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => {
- error!("pictshare err: {}", e);
+ error!("pictrs err: {}", e);
None
}
},
// Try to generate a small thumbnail if iframely is not supported
- None => match fetch_pictshare(&url) {
- Ok(res) => Some(res.url),
+ None => match fetch_pictrs(&url) {
+ Ok(res) => Some(res.files[0].file.to_owned()),
Err(e) => {
- error!("pictshare err: {}", e);
+ error!("pictrs err: {}", e);
None
}
},
iframely_title,
iframely_description,
iframely_html,
- pictshare_thumbnail,
+ pictrs_thumbnail,
)
}
None => (None, None, None, None),
-pub const VERSION: &str = "v0.6.74";
+pub const VERSION: &str = "v0.6.79";
setupTribute,
wsJsonToRes,
emojiPicker,
+ pictrsDeleteToast,
} from '../utils';
import { WebSocketService, UserService } from '../services';
import autosize from 'autosize';
</button>
{this.state.commentForm.content && (
<button
- className={`btn btn-sm mr-2 btn-secondary ${this.state
- .previewMode && 'active'}`}
+ className={`btn btn-sm mr-2 btn-secondary ${
+ this.state.previewMode && 'active'
+ }`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
{i18n.t('preview')}
file = event;
}
- const imageUploadUrl = `/pictshare/api/upload.php`;
+ const imageUploadUrl = `/pictrs/image`;
const formData = new FormData();
- formData.append('file', file);
+ formData.append('images[]', file);
i.state.imageLoading = true;
i.setState(i.state);
})
.then(res => res.json())
.then(res => {
- let url = `${window.location.origin}/pictshare/${res.url}`;
- let imageMarkdown =
- res.filetype == 'mp4' ? `[vid](${url}/raw)` : `![](${url})`;
- let content = i.state.commentForm.content;
- content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
- i.state.commentForm.content = content;
- i.state.imageLoading = false;
- i.setState(i.state);
- let textarea: any = document.getElementById(i.id);
- autosize.update(textarea);
+ console.log('pictrs upload:');
+ console.log(res);
+ if (res.msg == 'ok') {
+ let hash = res.files[0].file;
+ let url = `${window.location.origin}/pictrs/image/${hash}`;
+ let deleteToken = res.files[0].delete_token;
+ let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
+ let imageMarkdown = `![](${url})`;
+ let content = i.state.commentForm.content;
+ content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
+ i.state.commentForm.content = content;
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ let textarea: any = document.getElementById(i.id);
+ autosize.update(textarea);
+ pictrsDeleteToast(
+ i18n.t('click_to_delete_picture'),
+ i18n.t('picture_deleted'),
+ deleteUrl
+ );
+ } else {
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ toast(JSON.stringify(res), 'danger');
+ }
})
.catch(error => {
i.state.imageLoading = false;
} from '../interfaces';
import {
wsJsonToRes,
- pictshareAvatarThumbnail,
+ pictrsAvatarThumbnail,
showAvatars,
fetchLimit,
isCommentType,
<span>
{UserService.Instance.user.avatar && showAvatars() && (
<img
- src={pictshareAvatarThumbnail(
+ src={pictrsAvatarThumbnail(
UserService.Instance.user.avatar
)}
height="32"
requestNotificationPermission() {
if (UserService.Instance.user) {
- document.addEventListener('DOMContentLoaded', function() {
+ document.addEventListener('DOMContentLoaded', function () {
if (!Notification) {
toast(i18n.t('notifications_error'), 'danger');
return;
setupTribute,
setupTippy,
emojiPicker,
+ pictrsDeleteToast,
} from '../utils';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
file = event;
}
- const imageUploadUrl = `/pictshare/api/upload.php`;
+ const imageUploadUrl = `/pictrs/image`;
const formData = new FormData();
- formData.append('file', file);
+ formData.append('images[]', file);
i.state.imageLoading = true;
i.setState(i.state);
})
.then(res => res.json())
.then(res => {
- let url = `${window.location.origin}/pictshare/${encodeURI(res.url)}`;
- if (res.filetype == 'mp4') {
- url += '/raw';
+ console.log('pictrs upload:');
+ console.log(res);
+ if (res.msg == 'ok') {
+ let hash = res.files[0].file;
+ let url = `${window.location.origin}/pictrs/image/${hash}`;
+ let deleteToken = res.files[0].delete_token;
+ let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
+ i.state.postForm.url = url;
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ pictrsDeleteToast(
+ i18n.t('click_to_delete_picture'),
+ i18n.t('picture_deleted'),
+ deleteUrl
+ );
+ } else {
+ i.state.imageLoading = false;
+ i.setState(i.state);
+ toast(JSON.stringify(res), 'danger');
}
- i.state.postForm.url = url;
- i.state.imageLoading = false;
- i.setState(i.state);
})
.catch(error => {
i.state.imageLoading = false;
isImage,
isVideo,
getUnixTime,
- pictshareImage,
+ pictrsImage,
setupTippy,
previewLines,
} from '../utils';
getImage(thumbnail: boolean = false) {
let post = this.props.post;
if (isImage(post.url)) {
- if (post.url.includes('pictshare')) {
- return pictshareImage(post.url, thumbnail);
+ if (post.url.includes('pictrs')) {
+ return pictrsImage(post.url, thumbnail);
} else if (post.thumbnail_url) {
- return pictshareImage(post.thumbnail_url, thumbnail);
+ return pictrsImage(post.thumbnail_url, thumbnail);
} else {
return post.url;
}
} else if (post.thumbnail_url) {
- return pictshareImage(post.thumbnail_url, thumbnail);
+ return pictrsImage(post.thumbnail_url, thumbnail);
}
}
EditPrivateMessageForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import {
- mdToHtml,
- pictshareAvatarThumbnail,
- showAvatars,
- toast,
-} from '../utils';
+import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
import { MomentTime } from './moment-time';
import { PrivateMessageForm } from './private-message-form';
import { i18n } from '../i18next';
<img
height="32"
width="32"
- src={pictshareAvatarThumbnail(
+ src={pictrsAvatarThumbnail(
this.mine
? message.recipient_avatar
: message.creator_avatar
}
>
<svg
- class={`icon icon-inline ${message.read &&
- 'text-success'}`}
+ class={`icon icon-inline ${
+ message.read && 'text-success'
+ }`}
>
<use xlinkHref="#icon-check"></use>
</svg>
}
>
<svg
- class={`icon icon-inline ${message.deleted &&
- 'text-danger'}`}
+ class={`icon icon-inline ${
+ message.deleted && 'text-danger'
+ }`}
>
<use xlinkHref="#icon-trash"></use>
</svg>
data-tippy-content={i18n.t('view_source')}
>
<svg
- class={`icon icon-inline ${this.state.viewSource &&
- 'text-success'}`}
+ class={`icon icon-inline ${
+ this.state.viewSource && 'text-success'
+ }`}
>
<use xlinkHref="#icon-file-text"></use>
</svg>
fetchLimit,
routeSearchTypeToEnum,
routeSortTypeToEnum,
- pictshareAvatarThumbnail,
+ pictrsAvatarThumbnail,
showAvatars,
toast,
createCommentLikeRes,
import {
mdToHtml,
getUnixTime,
- pictshareAvatarThumbnail,
+ pictrsAvatarThumbnail,
showAvatars,
} from '../utils';
import { CommunityForm } from './community-form';
import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { UserView } from '../interfaces';
-import { pictshareAvatarThumbnail, showAvatars } from '../utils';
+import { pictrsAvatarThumbnail, showAvatars } from '../utils';
interface UserOther {
name: string;
<img
height="32"
width="32"
- src={pictshareAvatarThumbnail(user.avatar)}
+ src={pictrsAvatarThumbnail(user.avatar)}
class="rounded-circle mr-2"
/>
)}
handleImageUpload(i: User, event: any) {
event.preventDefault();
let file = event.target.files[0];
- const imageUploadUrl = `/pictshare/api/upload.php`;
+ const imageUploadUrl = `/pictrs/image`;
const formData = new FormData();
- formData.append('file', file);
+ formData.append('images[]', file);
i.state.avatarLoading = true;
i.setState(i.state);
})
.then(res => res.json())
.then(res => {
- let url = `${window.location.origin}/pictshare/${res.url}`;
- if (res.filetype == 'mp4') {
- url += '/raw';
+ console.log('pictrs upload:');
+ console.log(res);
+ if (res.msg == 'ok') {
+ let hash = res.files[0].file;
+ let url = `${window.location.origin}/pictrs/image/${hash}`;
+ i.state.userSettingsForm.avatar = url;
+ i.state.avatarLoading = false;
+ i.setState(i.state);
+ } else {
+ i.state.avatarLoading = false;
+ i.setState(i.state);
+ toast(JSON.stringify(res), 'danger');
}
- i.state.userSettingsForm.avatar = url;
- console.log(url);
- i.state.avatarLoading = false;
- i.setState(i.state);
})
.catch(error => {
i.state.avatarLoading = false;
}
// if the user is not logged in, we load the default themes and let the browser decide
- if(!loggedIn) {
- document.getElementById("default-light").removeAttribute('disabled')
- document.getElementById("default-dark").removeAttribute('disabled')
+ if (!loggedIn) {
+ document.getElementById('default-light').removeAttribute('disabled');
+ document.getElementById('default-dark').removeAttribute('disabled');
} else {
- document.getElementById("default-light").setAttribute('disabled', 'disabled');
- document.getElementById("default-dark").setAttribute('disabled', 'disabled');
+ document
+ .getElementById('default-light')
+ .setAttribute('disabled', 'disabled');
+ document
+ .getElementById('default-dark')
+ .setAttribute('disabled', 'disabled');
// Load the theme dynamically
let cssLoc = `/static/assets/css/themes/${theme}.min.css`;
return ret;
}
-export function pictshareAvatarThumbnail(src: string): string {
- // sample url: http://localhost:8535/pictshare/gs7xuu.jpg
- let split = src.split('pictshare');
- let out = `${split[0]}pictshare/${canUseWebP() ? 'webp/' : ''}96${split[1]}`;
+export function pictrsAvatarThumbnail(src: string): string {
+ // sample url: http://localhost:8535/pictrs/image/thumbnail256/gs7xuu.jpg
+ let split = src.split('/pictrs/image');
+ let out = `${split[0]}/pictrs/image/${
+ canUseWebP() ? 'webp/' : ''
+ }thumbnail96${split[1]}`;
return out;
}
}
// Converts to image thumbnail
-export function pictshareImage(
- hash: string,
- thumbnail: boolean = false
-): string {
- let root = `/pictshare`;
+export function pictrsImage(hash: string, thumbnail: boolean = false): string {
+ let root = `/pictrs/image`;
// Necessary for other servers / domains
- if (hash.includes('pictshare')) {
- let split = hash.split('/pictshare/');
- root = `${split[0]}/pictshare`;
+ if (hash.includes('pictrs')) {
+ let split = hash.split('/pictrs/image/');
+ root = `${split[0]}/pictrs/image`;
hash = split[1];
}
let out = `${root}/${canUseWebP() ? 'webp/' : ''}${
- thumbnail ? '192/' : ''
+ thumbnail ? 'thumbnail256/' : ''
}${hash}`;
return out;
}
}).showToast();
}
+export function pictrsDeleteToast(
+ clickToDeleteText: string,
+ deletePictureText: string,
+ deleteUrl: string
+) {
+ let backgroundColor = `var(--light)`;
+ let toast = Toastify({
+ text: clickToDeleteText,
+ backgroundColor: backgroundColor,
+ gravity: 'top',
+ position: 'right',
+ duration: 0,
+ onClick: () => {
+ if (toast) {
+ window.location.replace(deleteUrl);
+ alert(deletePictureText);
+ toast.hideToast();
+ }
+ },
+ close: true,
+ }).showToast();
+}
+
export function messageToastify(
creator: string,
avatar: string,
-export const version: string = 'v0.6.74';
+export const version: string = 'v0.6.79';
"delete_account": "Delete Account",
"delete_account_confirm":
"Warning: this will permanently delete all your data. Enter your password to confirm.",
+ "click_to_delete_picture": "Click to delete picture.",
+ "picture_deleted": "Picture deleted.",
"restore": "restore",
"ban": "ban",
"ban_from_site": "ban from site",
"create_a_post": "Crear una publicación",
"create_post": "Crear Publicación",
"number_of_posts": "{{count}} Publicación",
- "number_of_posts_plural": "{{count}} Publicaciónes",
+ "number_of_posts_plural": "{{count}} Publicaciones",
"posts": "Publicaciones",
"related_posts": "Estas publicaciones podrían estar relacionadas",
"cross_posts": "Este link también ha sido publicado en:",
"remove_as_admin": "eliminar como administrador",
"appoint_as_admin": "designar como administrador",
"remove": "eliminar",
- "removed": "eliminado",
+ "removed": "eliminado por moderador",
"locked": "bloqueado",
"stickied": "fijado",
"reason": "Razón",
"mark_as_read": "marcar como leído",
"mark_as_unread": "marcar como no leído",
"delete": "eliminar",
- "deleted": "eliminado",
+ "deleted": "eliminado por creador",
"delete_account": "Eliminar Cuenta",
- "delete_account_confirm": "Aviso: esta acción eliminará permanentemente tu información. Introduce tu contraseña para continuar",
+ "delete_account_confirm": "Advertencia: esta acción eliminará permanentemente toda tu información. Introduce tu contraseña para confirmar.",
"restore": "restaurar",
"ban": "expulsar",
"ban_from_site": "expulsar del sitio",
"theme": "Tema",
"sponsors": "Patrocinadores",
"sponsors_of_lemmy": "Patrocinadores de Lemmy",
- "sponsor_message": "Lemmy es software libre y de <1>código abierto</1>, lo que significa que no tendrá publicidades, monetización, ni capitales emprendedores, nunca. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:",
+ "sponsor_message": "Lemmy es software libre y de <1>código abierto</1>, lo que significa que nunca tendrá publicidad, monetización, ni capitales emprendedores. Tus donaciones apoyan directamente el desarrollo a tiempo completo del proyecto. Muchas gracias a las siguientes personas:",
"support_on_patreon": "Apoyo en Patreon",
"support_on_liberapay": "Apoyo en Liberapay",
"donate_to_lemmy": "Donar a Lemmy",
"banned_users": "Usuarios Baneados",
"support_on_open_collective": "Dona en OpenCollective",
"site_saved": "Sitio Guardado.",
- "emoji_picker": "Emoji Picker",
- "admin_settings": "Panel de Administración"
+ "emoji_picker": "Lista de emojis",
+ "admin_settings": "Panel de Administración",
+ "select_a_community": "Selecciona una comunidad",
+ "invalid_username": "Nombre de usuario inválido."
}
"preview": "prévisualiser",
"upload_image": "envoyer une image",
"avatar": "Avatar",
- "upload_avatar": "Télécharger une avatar",
+ "upload_avatar": "Télécharger un avatar",
"show_avatars": "Afficher les avatars",
"formatting_help": "aide au formattage",
"view_source": "voir la source",
"post": "Elküld",
"remove_post": "Bejegyzés eltávolítása",
"no_posts": "Nincs bejegyzés.",
- "create_post": "Új bejegyzés létrehozása",
- "create_a_post": "Új bejegyzés létrehozása",
+ "create_post": "Bejegyzés létrehozása",
+ "create_a_post": "Bejegyzés létrehozása",
"number_of_posts": "{{count}} bejegyzés",
"number_of_posts_plural": "{{count}} bejegyzés",
"posts": "Bejegyzések",
"remove_comment": "Hozzászólások eltávolítása",
"cross_posted_to": "beküldve ide is: ",
"number_of_comments": "{{count}} hozzászólás",
- "number_of_comments_plural": "{{count}} hozzászólás"
+ "number_of_comments_plural": "{{count}} hozzászólás",
+ "communities": "Közösségek",
+ "users": "Felhasználók",
+ "create_a_community": "Közösség létrehozása",
+ "select_a_community": "Közösség kiválasztása",
+ "create_community": "Közösség létrehozása",
+ "remove_community": "Közösség eltávolítása",
+ "trending_communities": "Népszerű <1>közösségek</1>",
+ "list_of_communities": "Közösségek listája",
+ "community_reqs": "Kisbetű és alsóvonás megengedett, szóköz nem.",
+ "create_private_message": "Privát üzenet létrehozása",
+ "send_secure_message": "Biztonságos üzenet küldése",
+ "send_message": "Üzenet küldése",
+ "message": "Üzenet",
+ "edit": "szerkesztés",
+ "reply": "válasz",
+ "more": "több",
+ "cancel": "Mégse",
+ "preview": "Előnézet",
+ "upload_image": "kép feltöltése",
+ "avatar": "Avatár",
+ "upload_avatar": "Avatár feltöltése",
+ "show_avatars": "Avatárok mutatása",
+ "show_context": "Összefüggés mutatása",
+ "sorting_help": "rendezési segítség",
+ "view_source": "forrás megtekintése",
+ "unlock": "zárolás feloldása",
+ "lock": "zárolás",
+ "sticky": "rögzítés",
+ "unsticky": "rögzítés feloldása",
+ "link": "hivatkozás",
+ "mod": "moderátor",
+ "mods": "moderátorok",
+ "moderates": "Moderált közösségek",
+ "settings": "Beállítások",
+ "admin_settings": "Adminisztrációs beállítások",
+ "remove_as_mod": "moderátori jog eltávolítása",
+ "appoint_as_mod": "kinevezés moderátornak",
+ "modlog": "Moderációs napló",
+ "admin": "admin",
+ "admins": "adminok",
+ "remove_as_admin": "adminjog eltávolítása",
+ "appoint_as_admin": "kinevezés adminnak",
+ "remove": "eltávolítás",
+ "locked": "zárolva",
+ "stickied": "rögzítve",
+ "reason": "Indok",
+ "mark_as_read": "megjelölés olvasottnak",
+ "mark_as_unread": "megjelölés olvasatlannak",
+ "delete": "törlés",
+ "deleted": "eltávolítva a szerző által",
+ "delete_account": "FIók törlése",
+ "restore": "visszaállítás",
+ "ban": "kitiltás",
+ "ban_from_site": "kitiltás az oldalról",
+ "unban": "kitiltás visszavonása",
+ "unban_from_site": "az oldalról történő kitiltás visszavonása",
+ "banned": "kitiltva",
+ "banned_users": "Kitiltott felhasználók",
+ "save": "mentés",
+ "unsave": "mentés visszavonása",
+ "create": "létrehozás",
+ "creator": "szerző",
+ "username": "Felhasználónév",
+ "number_of_points": "{{count}} pont",
+ "number_of_points_plural": "{{count}} pont",
+ "number_of_subscribers": "{{count}} feliratkozó",
+ "number_of_subscribers_plural": "{{count}} feliratkozó",
+ "name": "Név",
+ "title": "Cím",
+ "category": "Kategória",
+ "both": "Mindkettő",
+ "saved": "Mentve",
+ "unsubscribe": "Leiratkozás",
+ "subscribe": "Feliratkozás",
+ "subscribed": "Feliratkozva",
+ "subscribed_to_communities": "Követett <1>közösségek</1>",
+ "number_of_communities": "{{count}} közösség",
+ "number_of_communities_plural": "{{count}} közösség",
+ "formatting_help": "formázási segítség",
+ "archive_link": "hivatkozás archiválása",
+ "site_config": "Oldalbeállítások",
+ "removed": "eltávolítva egy mod által",
+ "delete_account_confirm": "Figyelmeztetés: ez véglegesen törölni fogja az összes adatodat. A megerősítéshez írd be a jelszavad!",
+ "email_or_username": "Email vagy felhasználónév",
+ "number_of_users": "{{count}} felhasználó",
+ "number_of_users_plural": "{{count}} felhasználó",
+ "number_online": "{{count}} online felhasználó",
+ "number_online_plural": "{{count}} online felhasználó",
+ "subscribers": "Feliratkozók"
}
"remove_as_admin": "移除管理权限",
"appoint_as_admin": "添加管理权限",
"remove": "移除",
- "removed": "已移除",
+ "removed": "已被管理员移除",
"locked": "已加锁",
"reason": "原因",
"mark_as_read": "标记未读",
"mark_as_unread": "标记已读",
"delete": "删除",
- "deleted": "已删除",
+ "deleted": "作者已删除",
"restore": "恢复",
"ban": "禁止",
"ban_from_site": "禁止此站点",
"time": "时间",
"action": "行动",
"block_leaving": "确定要离开吗?",
- "show_context": "显示上下文"
+ "show_context": "显示上下文",
+ "admin_settings": "管理员设置",
+ "site_config": "网站配置",
+ "banned_users": "被禁止用户",
+ "site_saved": "网站已保存",
+ "emoji_picker": "选择表情",
+ "invalid_username": "用户名无效"
}