Merge branch 'master' into iav-arm-musl-dessalines

This commit is contained in:
Dessalines 2020-06-22 14:57:55 -04:00
commit 983a45e178
31 changed files with 466 additions and 165 deletions

34
RELEASES.md vendored
View file

@ -1,3 +1,37 @@
# 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)

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.6.77
v0.6.79

1
ansible/ansible.cfg vendored
View file

@ -1,5 +1,6 @@
[defaults]
inventory=inventory
interpreter_python=/usr/bin/python3
[ssh_connection]
pipelining = True

8
ansible/lemmy.yml vendored
View file

@ -24,10 +24,11 @@
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
@ -59,6 +60,7 @@
project_src: /lemmy/
state: present
pull: yes
remove_orphans: yes
- name: reload nginx with new config
shell: nginx -s reload

View file

@ -26,10 +26,11 @@
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
@ -88,6 +89,7 @@
project_src: /lemmy/
state: present
recreate: always
remove_orphans: yes
ignore_errors: yes
- name: reload nginx with new config

View file

@ -12,7 +12,7 @@ services:
- ./lemmy.hjson:/config/config.hjson:ro
depends_on:
- postgres
- pictshare
- pictrs
- iframely
postgres:
@ -25,12 +25,13 @@ services:
- ./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:

View file

@ -48,8 +48,8 @@ server {
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;
@ -70,15 +70,21 @@ server {
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/ {

View file

@ -1,15 +1,6 @@
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:
@ -23,16 +14,27 @@ services:
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/pictshare:/usr/share/nginx/html/data
- ./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/pictrs:/mnt
restart: always
iframely:

View file

@ -12,7 +12,7 @@ services:
restart: always
lemmy:
image: dessalines/lemmy:v0.6.77
image: dessalines/lemmy:v0.6.79
ports:
- "127.0.0.1:8536:8536"
restart: always
@ -22,17 +22,17 @@ services:
- ./lemmy.hjson:/config/config.hjson
depends_on:
- postgres
- pictshare
- pictrs
- iframely
pictshare:
image: hascheksolutions/pictshare:latest
pictrs:
image: asonix/pictrs:v0.1.13-r0
ports:
- "127.0.0.1:8537:80"
- "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

View file

@ -0,0 +1,60 @@
#!/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

View file

@ -1,4 +1,5 @@
use super::*;
use crate::is_valid_community_name;
#[derive(Serialize, Deserialize)]
pub struct GetCommunity {
@ -220,6 +221,10 @@ impl Perform for Oper<CreateCommunity> {
}
}
if !is_valid_community_name(&data.name) {
return Err(APIError::err("invalid_community_name").into());
}
let user_id = claims.id;
let conn = pool.get()?;
@ -306,6 +311,10 @@ impl Perform for Oper<EditCommunity> {
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
if !is_valid_community_name(&data.name) {
return Err(APIError::err("invalid_community_name").into());
}
let user_id = claims.id;
let conn = pool.get()?;

View file

@ -18,7 +18,7 @@ use crate::db::user_mention_view::*;
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,
};

View file

@ -116,9 +116,9 @@ impl Perform for Oper<CreatePost> {
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(),
@ -135,7 +135,7 @@ impl Perform for Oper<CreatePost> {
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) {
@ -450,9 +450,9 @@ impl Perform for Oper<EditPost> {
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(),
@ -469,7 +469,7 @@ impl Perform for Oper<EditPost> {
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) {

View file

@ -187,25 +187,35 @@ pub fn fetch_iframely(url: &str) -> Result<IframelyResponse, failure::Error> {
Ok(res)
}
#[derive(Deserialize, Debug)]
pub struct PictshareResponse {
status: String,
url: String,
#[derive(Deserialize, Debug, Clone)]
pub struct PictrsResponse {
files: Vec<PictrsFile>,
msg: String,
}
pub fn fetch_pictshare(image_url: &str) -> Result<PictshareResponse, failure::Error> {
#[derive(Deserialize, Debug, Clone)]
pub struct PictrsFile {
file: String,
delete_token: String,
}
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>,
@ -225,20 +235,20 @@ fn fetch_iframely_and_pictshare_data(
}
};
// 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
}
},
@ -248,7 +258,7 @@ fn fetch_iframely_and_pictshare_data(
iframely_title,
iframely_description,
iframely_html,
pictshare_thumbnail,
pictrs_thumbnail,
)
}
None => (None, None, None, None),
@ -273,11 +283,15 @@ pub fn is_valid_username(name: &str) -> bool {
VALID_USERNAME_REGEX.is_match(name)
}
pub fn is_valid_community_name(name: &str) -> bool {
VALID_COMMUNITY_NAME_REGEX.is_match(name)
}
#[cfg(test)]
mod tests {
use crate::{
extract_usernames, is_email_regex, is_image_content_type, is_valid_username, remove_slurs,
slur_check, slurs_vec_to_str,
extract_usernames, is_email_regex, is_image_content_type, is_valid_community_name,
is_valid_username, remove_slurs, slur_check, slurs_vec_to_str,
};
#[test]
@ -304,6 +318,15 @@ mod tests {
assert!(!is_valid_username(""));
}
#[test]
fn test_valid_community_name() {
assert!(is_valid_community_name("example"));
assert!(is_valid_community_name("example_community"));
assert!(!is_valid_community_name("Example"));
assert!(!is_valid_community_name("Ex"));
assert!(!is_valid_community_name(""));
}
#[test]
fn test_slur_filter() {
let test =
@ -366,4 +389,5 @@ lazy_static! {
static ref SLUR_REGEX: Regex = RegexBuilder::new(r"(fag(g|got|tard)?|maricos?|cock\s?sucker(s|ing)?|nig(\b|g?(a|er)?(s|z)?)\b|dindu(s?)|mudslime?s?|kikes?|mongoloids?|towel\s*heads?|\bspi(c|k)s?\b|\bchinks?|niglets?|beaners?|\bnips?\b|\bcoons?\b|jungle\s*bunn(y|ies?)|jigg?aboo?s?|\bpakis?\b|rag\s*heads?|gooks?|cunts?|bitch(es|ing|y)?|puss(y|ies?)|twats?|feminazis?|whor(es?|ing)|\bslut(s|t?y)?|\btrann?(y|ies?)|ladyboy(s?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().unwrap();
static ref USERNAME_MATCHES_REGEX: Regex = Regex::new(r"/u/[a-zA-Z][0-9a-zA-Z_]*").unwrap();
static ref VALID_USERNAME_REGEX: Regex = Regex::new(r"^[a-zA-Z0-9_]{3,20}$").unwrap();
static ref VALID_COMMUNITY_NAME_REGEX: Regex = Regex::new(r"^[a-z0-9_]{3,20}$").unwrap();
}

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.6.77";
pub const VERSION: &str = "v0.6.79";

View file

@ -18,6 +18,7 @@ import {
setupTribute,
wsJsonToRes,
emojiPicker,
pictrsDeleteToast,
} from '../utils';
import { WebSocketService, UserService } from '../services';
import autosize from 'autosize';
@ -162,8 +163,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
</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')}
@ -304,9 +306,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
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);
@ -317,16 +319,31 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
})
.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;

View file

@ -22,7 +22,7 @@ import {
} from '../interfaces';
import {
wsJsonToRes,
pictshareAvatarThumbnail,
pictrsAvatarThumbnail,
showAvatars,
fetchLimit,
isCommentType,
@ -218,7 +218,7 @@ export class Navbar extends Component<any, NavbarState> {
<span>
{UserService.Instance.user.avatar && showAvatars() && (
<img
src={pictshareAvatarThumbnail(
src={pictrsAvatarThumbnail(
UserService.Instance.user.avatar
)}
height="32"
@ -381,7 +381,7 @@ export class Navbar extends Component<any, NavbarState> {
requestNotificationPermission() {
if (UserService.Instance.user) {
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
if (!Notification) {
toast(i18n.t('notifications_error'), 'danger');
return;

View file

@ -35,6 +35,7 @@ import {
setupTribute,
setupTippy,
emojiPicker,
pictrsDeleteToast,
} from '../utils';
import autosize from 'autosize';
import Tribute from 'tributejs/src/Tribute.js';
@ -518,9 +519,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
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);
@ -531,13 +532,26 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
})
.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;

View file

@ -28,7 +28,7 @@ import {
isImage,
isVideo,
getUnixTime,
pictshareImage,
pictrsImage,
setupTippy,
previewLines,
} from '../utils';
@ -161,15 +161,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
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);
}
}

View file

@ -5,12 +5,7 @@ import {
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';
@ -78,7 +73,7 @@ export class PrivateMessage extends Component<
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(
src={pictrsAvatarThumbnail(
this.mine
? message.recipient_avatar
: message.creator_avatar
@ -144,8 +139,9 @@ export class PrivateMessage extends Component<
}
>
<svg
class={`icon icon-inline ${message.read &&
'text-success'}`}
class={`icon icon-inline ${
message.read && 'text-success'
}`}
>
<use xlinkHref="#icon-check"></use>
</svg>
@ -188,8 +184,9 @@ export class PrivateMessage extends Component<
}
>
<svg
class={`icon icon-inline ${message.deleted &&
'text-danger'}`}
class={`icon icon-inline ${
message.deleted && 'text-danger'
}`}
>
<use xlinkHref="#icon-trash"></use>
</svg>
@ -204,8 +201,9 @@ export class PrivateMessage extends Component<
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>

View file

@ -22,7 +22,7 @@ import {
fetchLimit,
routeSearchTypeToEnum,
routeSortTypeToEnum,
pictshareAvatarThumbnail,
pictrsAvatarThumbnail,
showAvatars,
toast,
createCommentLikeRes,

View file

@ -11,7 +11,7 @@ import { WebSocketService, UserService } from '../services';
import {
mdToHtml,
getUnixTime,
pictshareAvatarThumbnail,
pictrsAvatarThumbnail,
showAvatars,
} from '../utils';
import { CommunityForm } from './community-form';

View file

@ -1,7 +1,7 @@
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;
@ -25,7 +25,7 @@ export class UserListing extends Component<UserListingProps, any> {
<img
height="32"
width="32"
src={pictshareAvatarThumbnail(user.avatar)}
src={pictrsAvatarThumbnail(user.avatar)}
class="rounded-circle mr-2"
/>
)}

View file

@ -988,9 +988,9 @@ export class User extends Component<any, UserState> {
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);
@ -1001,14 +1001,19 @@ export class User extends Component<any, UserState> {
})
.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;

62
ui/src/utils.ts vendored
View file

@ -414,12 +414,16 @@ export function setTheme(theme: string = 'darkly', loggedIn: boolean = 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`;
@ -449,10 +453,12 @@ export function objectFlip(obj: any) {
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;
}
@ -464,21 +470,18 @@ export function showAvatars(): boolean {
}
// 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;
}
@ -497,6 +500,29 @@ export function toast(text: string, background: string = 'success') {
}).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,

2
ui/src/version.ts vendored
View file

@ -1 +1 @@
export const version: string = 'v0.6.77';
export const version: string = 'v0.6.79';

View file

@ -27,6 +27,7 @@
"number_of_communities": "{{count}} Community",
"number_of_communities_plural": "{{count}} Communities",
"community_reqs": "lowercase, underscores, and no spaces.",
"invalid_community_name": "Invalid name.",
"create_private_message": "Create Private Message",
"send_secure_message": "Send Secure Message",
"send_message": "Send Message",
@ -75,6 +76,8 @@
"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",

View file

@ -5,7 +5,7 @@
"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:",
@ -57,16 +57,16 @@
"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",
@ -169,7 +169,7 @@
"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",
@ -250,6 +250,8 @@
"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."
}

View file

@ -36,7 +36,7 @@
"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",

View file

@ -2,8 +2,8 @@
"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",
@ -14,5 +14,94 @@
"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"
}

View file

@ -35,13 +35,13 @@
"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": "禁止此站点",
@ -235,5 +235,11 @@
"time": "时间",
"action": "行动",
"block_leaving": "确定要离开吗?",
"show_context": "显示上下文"
"show_context": "显示上下文",
"admin_settings": "管理员设置",
"site_config": "网站配置",
"banned_users": "被禁止用户",
"site_saved": "网站已保存",
"emoji_picker": "选择表情",
"invalid_username": "用户名无效"
}