mirror of https://github.com/LemmyNet/lemmy
Merge branch 'private_messaging' into dev
commit
0d2b4e6f18
@ -0,0 +1,34 @@
|
||||
-- Drop the triggers
|
||||
drop trigger refresh_private_message on private_message;
|
||||
drop function refresh_private_message();
|
||||
|
||||
-- Drop the view and table
|
||||
drop view private_message_view cascade;
|
||||
drop table private_message;
|
||||
|
||||
-- Rebuild the old views
|
||||
drop view user_view cascade;
|
||||
create view user_view as
|
||||
select
|
||||
u.id,
|
||||
u.name,
|
||||
u.avatar,
|
||||
u.email,
|
||||
u.fedi_name,
|
||||
u.admin,
|
||||
u.banned,
|
||||
u.show_avatars,
|
||||
u.send_notifications_to_email,
|
||||
u.published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
||||
|
||||
create materialized view user_mview as select * from user_view;
|
||||
|
||||
create unique index idx_user_mview_id on user_mview (id);
|
||||
|
||||
-- Drop the columns
|
||||
alter table user_ drop column matrix_user_id;
|
@ -0,0 +1,90 @@
|
||||
-- Creating private message
|
||||
create table private_message (
|
||||
id serial primary key,
|
||||
creator_id int references user_ on update cascade on delete cascade not null,
|
||||
recipient_id int references user_ on update cascade on delete cascade not null,
|
||||
content text not null,
|
||||
deleted boolean default false not null,
|
||||
read boolean default false not null,
|
||||
published timestamp not null default now(),
|
||||
updated timestamp
|
||||
);
|
||||
|
||||
-- Create the view and materialized view which has the avatar and creator name
|
||||
create view private_message_view as
|
||||
select
|
||||
pm.*,
|
||||
u.name as creator_name,
|
||||
u.avatar as creator_avatar,
|
||||
u2.name as recipient_name,
|
||||
u2.avatar as recipient_avatar
|
||||
from private_message pm
|
||||
inner join user_ u on u.id = pm.creator_id
|
||||
inner join user_ u2 on u2.id = pm.recipient_id;
|
||||
|
||||
create materialized view private_message_mview as select * from private_message_view;
|
||||
|
||||
create unique index idx_private_message_mview_id on private_message_mview (id);
|
||||
|
||||
-- Create the triggers
|
||||
create or replace function refresh_private_message()
|
||||
returns trigger language plpgsql
|
||||
as $$
|
||||
begin
|
||||
refresh materialized view concurrently private_message_mview;
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
create trigger refresh_private_message
|
||||
after insert or update or delete or truncate
|
||||
on private_message
|
||||
for each statement
|
||||
execute procedure refresh_private_message();
|
||||
|
||||
-- Update user to include matrix id
|
||||
alter table user_ add column matrix_user_id text unique;
|
||||
|
||||
drop view user_view cascade;
|
||||
create view user_view as
|
||||
select
|
||||
u.id,
|
||||
u.name,
|
||||
u.avatar,
|
||||
u.email,
|
||||
u.matrix_user_id,
|
||||
u.fedi_name,
|
||||
u.admin,
|
||||
u.banned,
|
||||
u.show_avatars,
|
||||
u.send_notifications_to_email,
|
||||
u.published,
|
||||
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
|
||||
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
|
||||
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
|
||||
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
|
||||
from user_ u;
|
||||
|
||||
create materialized view user_mview as select * from user_view;
|
||||
|
||||
create unique index idx_user_mview_id on user_mview (id);
|
||||
|
||||
-- This is what a group pm table would look like
|
||||
-- Not going to do it now because of the complications
|
||||
--
|
||||
-- create table private_message (
|
||||
-- id serial primary key,
|
||||
-- creator_id int references user_ on update cascade on delete cascade not null,
|
||||
-- content text not null,
|
||||
-- deleted boolean default false not null,
|
||||
-- published timestamp not null default now(),
|
||||
-- updated timestamp
|
||||
-- );
|
||||
--
|
||||
-- create table private_message_recipient (
|
||||
-- id serial primary key,
|
||||
-- private_message_id int references private_message on update cascade on delete cascade not null,
|
||||
-- recipient_id int references user_ on update cascade on delete cascade not null,
|
||||
-- read boolean default false not null,
|
||||
-- published timestamp not null default now(),
|
||||
-- unique(private_message_id, recipient_id)
|
||||
-- )
|
@ -0,0 +1,144 @@
|
||||
use super::*;
|
||||
use crate::schema::private_message;
|
||||
|
||||
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
|
||||
#[table_name = "private_message"]
|
||||
pub struct PrivateMessage {
|
||||
pub id: i32,
|
||||
pub creator_id: i32,
|
||||
pub recipient_id: i32,
|
||||
pub content: String,
|
||||
pub deleted: bool,
|
||||
pub read: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone)]
|
||||
#[table_name = "private_message"]
|
||||
pub struct PrivateMessageForm {
|
||||
pub creator_id: i32,
|
||||
pub recipient_id: i32,
|
||||
pub content: Option<String>,
|
||||
pub deleted: Option<bool>,
|
||||
pub read: Option<bool>,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
impl Crud<PrivateMessageForm> for PrivateMessage {
|
||||
fn read(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
private_message.find(private_message_id).first::<Self>(conn)
|
||||
}
|
||||
|
||||
fn delete(conn: &PgConnection, private_message_id: i32) -> Result<usize, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
diesel::delete(private_message.find(private_message_id)).execute(conn)
|
||||
}
|
||||
|
||||
fn create(conn: &PgConnection, private_message_form: &PrivateMessageForm) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
insert_into(private_message)
|
||||
.values(private_message_form)
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
|
||||
fn update(
|
||||
conn: &PgConnection,
|
||||
private_message_id: i32,
|
||||
private_message_form: &PrivateMessageForm,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::private_message::dsl::*;
|
||||
diesel::update(private_message.find(private_message_id))
|
||||
.set(private_message_form)
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::super::user::*;
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_crud() {
|
||||
let conn = establish_unpooled_connection();
|
||||
|
||||
let creator_form = UserForm {
|
||||
name: "creator_pm".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_creator = User_::create(&conn, &creator_form).unwrap();
|
||||
|
||||
let recipient_form = UserForm {
|
||||
name: "recipient_pm".into(),
|
||||
fedi_name: "rrf".into(),
|
||||
preferred_username: None,
|
||||
password_encrypted: "nope".into(),
|
||||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
show_nsfw: false,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
send_notifications_to_email: false,
|
||||
};
|
||||
|
||||
let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
|
||||
|
||||
let private_message_form = PrivateMessageForm {
|
||||
content: Some("A test private message".into()),
|
||||
creator_id: inserted_creator.id,
|
||||
recipient_id: inserted_recipient.id,
|
||||
deleted: None,
|
||||
read: None,
|
||||
updated: None,
|
||||
};
|
||||
|
||||
let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap();
|
||||
|
||||
let expected_private_message = PrivateMessage {
|
||||
id: inserted_private_message.id,
|
||||
content: "A test private message".into(),
|
||||
creator_id: inserted_creator.id,
|
||||
recipient_id: inserted_recipient.id,
|
||||
deleted: false,
|
||||
read: false,
|
||||
updated: None,
|
||||
published: inserted_private_message.published,
|
||||
};
|
||||
|
||||
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
|
||||
let updated_private_message =
|
||||
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
|
||||
let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
|
||||
User_::delete(&conn, inserted_creator.id).unwrap();
|
||||
User_::delete(&conn, inserted_recipient.id).unwrap();
|
||||
|
||||
assert_eq!(expected_private_message, read_private_message);
|
||||
assert_eq!(expected_private_message, updated_private_message);
|
||||
assert_eq!(expected_private_message, inserted_private_message);
|
||||
assert_eq!(1, num_deleted);
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
use super::*;
|
||||
use diesel::pg::Pg;
|
||||
|
||||
// The faked schema since diesel doesn't do views
|
||||
table! {
|
||||
private_message_view (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
content -> Text,
|
||||
deleted -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
recipient_name -> Varchar,
|
||||
recipient_avatar -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
private_message_mview (id) {
|
||||
id -> Int4,
|
||||
creator_id -> Int4,
|
||||
recipient_id -> Int4,
|
||||
content -> Text,
|
||||
deleted -> Bool,
|
||||
read -> Bool,
|
||||
published -> Timestamp,
|
||||
updated -> Nullable<Timestamp>,
|
||||
creator_name -> Varchar,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
recipient_name -> Varchar,
|
||||
recipient_avatar -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
|
||||
)]
|
||||
#[table_name = "private_message_view"]
|
||||
pub struct PrivateMessageView {
|
||||
pub id: i32,
|
||||
pub creator_id: i32,
|
||||
pub recipient_id: i32,
|
||||
pub content: String,
|
||||
pub deleted: bool,
|
||||
pub read: bool,
|
||||
pub published: chrono::NaiveDateTime,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub creator_name: String,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub recipient_name: String,
|
||||
pub recipient_avatar: Option<String>,
|
||||
}
|
||||
|
||||
pub struct PrivateMessageQueryBuilder<'a> {
|
||||
conn: &'a PgConnection,
|
||||
query: super::private_message_view::private_message_mview::BoxedQuery<'a, Pg>,
|
||||
for_recipient_id: i32,
|
||||
unread_only: bool,
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
}
|
||||
|
||||
impl<'a> PrivateMessageQueryBuilder<'a> {
|
||||
pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self {
|
||||
use super::private_message_view::private_message_mview::dsl::*;
|
||||
|
||||
let query = private_message_mview.into_boxed();
|
||||
|
||||
PrivateMessageQueryBuilder {
|
||||
conn,
|
||||
query,
|
||||
for_recipient_id,
|
||||
unread_only: false,
|
||||
page: None,
|
||||
limit: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unread_only(mut self, unread_only: bool) -> Self {
|
||||
self.unread_only = unread_only;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
|
||||
self.page = page.get_optional();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
|
||||
self.limit = limit.get_optional();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> {
|
||||
use super::private_message_view::private_message_mview::dsl::*;
|
||||
|
||||
let mut query = self.query;
|
||||
|
||||
// If its unread, I only want the ones to me
|
||||
if self.unread_only {
|
||||
query = query
|
||||
.filter(read.eq(false))
|
||||
.filter(recipient_id.eq(self.for_recipient_id));
|
||||
}
|
||||
// Otherwise, I want the ALL view to show both sent and received
|
||||
else {
|
||||
query = query.filter(
|
||||
recipient_id
|
||||
.eq(self.for_recipient_id)
|
||||
.or(creator_id.eq(self.for_recipient_id)),
|
||||
)
|
||||
}
|
||||
|
||||
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||
|
||||
query
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.order_by(published.desc())
|
||||
.load::<PrivateMessageView>(self.conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateMessageView {
|
||||
pub fn read(conn: &PgConnection, from_private_message_id: i32) -> Result<Self, Error> {
|
||||
use super::private_message_view::private_message_view::dsl::*;
|
||||
|
||||
let mut query = private_message_view.into_boxed();
|
||||
|
||||
query = query
|
||||
.filter(id.eq(from_private_message_id))
|
||||
.order_by(published.desc());
|
||||
|
||||
query.first::<Self>(conn)
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
import { Component } from 'inferno';
|
||||
import { PrivateMessageForm } from './private-message-form';
|
||||
import { WebSocketService } from '../services';
|
||||
import { PrivateMessageFormParams } from '../interfaces';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
export class CreatePrivateMessage extends Component<any, any> {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.title = `${i18n.t('create_private_message')} - ${
|
||||
WebSocketService.Instance.site.name
|
||||
}`;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-6 offset-lg-3 mb-4">
|
||||
<h5>{i18n.t('create_private_message')}</h5>
|
||||
<PrivateMessageForm
|
||||
onCreate={this.handlePrivateMessageCreate}
|
||||
params={this.params}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
get params(): PrivateMessageFormParams {
|
||||
let urlParams = new URLSearchParams(this.props.location.search);
|
||||
let params: PrivateMessageFormParams = {
|
||||
recipient_id: Number(urlParams.get('recipient_id')),
|
||||
};
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
handlePrivateMessageCreate() {
|
||||
alert(i18n.t('message_sent'));
|
||||
|
||||
// Navigate to the front
|
||||
this.props.history.push(`/`);
|
||||
}
|
||||
}
|
@ -0,0 +1,291 @@
|
||||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import {
|
||||
PrivateMessageForm as PrivateMessageFormI,
|
||||
EditPrivateMessageForm,
|
||||
PrivateMessageFormParams,
|
||||
PrivateMessage,
|
||||
PrivateMessageResponse,
|
||||
UserView,
|
||||
UserOperation,
|
||||
UserDetailsResponse,
|
||||
GetUserDetailsForm,
|
||||
SortType,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import {
|
||||
msgOp,
|
||||
capitalizeFirstLetter,
|
||||
markdownHelpUrl,
|
||||
mdToHtml,
|
||||
showAvatars,
|
||||
pictshareAvatarThumbnail,
|
||||
} from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
interface PrivateMessageFormProps {
|
||||
privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
|
||||
params?: PrivateMessageFormParams;
|
||||
onCancel?(): any;
|
||||
onCreate?(message: PrivateMessage): any;
|
||||
onEdit?(message: PrivateMessage): any;
|
||||
}
|
||||
|
||||
interface PrivateMessageFormState {
|
||||
privateMessageForm: PrivateMessageFormI;
|
||||
recipient: UserView;
|
||||
loading: boolean;
|
||||
previewMode: boolean;
|
||||
showDisclaimer: boolean;
|
||||
}
|
||||
|
||||
export class PrivateMessageForm extends Component<
|
||||
PrivateMessageFormProps,
|
||||
PrivateMessageFormState
|
||||
> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: PrivateMessageFormState = {
|
||||
privateMessageForm: {
|
||||
content: null,
|
||||
recipient_id: null,
|
||||
},
|
||||
recipient: null,
|
||||
loading: false,
|
||||
previewMode: false,
|
||||
showDisclaimer: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
if (this.props.privateMessage) {
|
||||
this.state.privateMessageForm = {
|
||||
content: this.props.privateMessage.content,
|
||||
recipient_id: this.props.privateMessage.recipient_id,
|
||||
};
|
||||
}
|
||||
|
||||
if (this.props.params) {
|
||||
this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
|
||||
let form: GetUserDetailsForm = {
|
||||
user_id: this.state.privateMessageForm.recipient_id,
|
||||
sort: SortType[SortType.New],
|
||||
saved_only: false,
|
||||
};
|
||||
WebSocketService.Instance.getUserDetails(form);
|
||||
}
|
||||
|
||||
this.subscription = WebSocketService.Instance.subject
|
||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||
.subscribe(
|
||||
msg => this.parseMessage(msg),
|
||||
err => console.error(err),
|
||||
() => console.log('complete')
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
autosize(document.querySelectorAll('textarea'));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
|
||||
{!this.props.privateMessage && (
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">
|
||||
{capitalizeFirstLetter(i18n.t('to'))}
|
||||
</label>
|
||||
|
||||
{this.state.recipient && (
|
||||
<div class="col-sm-10 form-control-plaintext">
|
||||
<Link
|
||||
className="text-info"
|
||||
to={`/u/${this.state.recipient.name}`}
|
||||
>
|
||||
{this.state.recipient.avatar && showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(
|
||||
this.state.recipient.avatar
|
||||
)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>{this.state.recipient.name}</span>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea
|
||||
value={this.state.privateMessageForm.content}
|
||||
onInput={linkEvent(this, this.handleContentChange)}
|
||||
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||
rows={4}
|
||||
maxLength={10000}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.privateMessageForm.content
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{this.state.privateMessageForm.content && (
|
||||
<button
|
||||
className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
|
||||
.previewMode && 'active'}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
</button>
|
||||
)}
|
||||
<ul class="float-right list-inline mb-1 text-muted small font-weight-bold">
|
||||
<li class="list-inline-item">
|
||||
<span
|
||||
onClick={linkEvent(this, this.handleShowDisclaimer)}
|
||||
class="pointer"
|
||||
>
|
||||
{i18n.t('disclaimer')}
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a href={markdownHelpUrl} target="_blank" class="text-muted">
|
||||
{i18n.t('formatting_help')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.state.showDisclaimer && (
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<T i18nKey="private_message_disclaimer">
|
||||
#
|
||||
<a
|
||||
class="alert-link"
|
||||
target="_blank"
|
||||
href="https://about.riot.im/"
|
||||
>
|
||||
#
|
||||
</a>
|
||||
</T>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-secondary mr-2">
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : this.props.privateMessage ? (
|
||||
capitalizeFirstLetter(i18n.t('save'))
|
||||
) : (
|
||||
capitalizeFirstLetter(i18n.t('send_message'))
|
||||
)}
|
||||
</button>
|
||||
{this.props.privateMessage && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
onClick={linkEvent(this, this.handleCancel)}
|
||||
>
|
||||
{i18n.t('cancel')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
|
||||
event.preventDefault();
|
||||
if (i.props.privateMessage) {
|
||||
let editForm: EditPrivateMessageForm = {
|
||||
edit_id: i.props.privateMessage.id,
|
||||
content: i.state.privateMessageForm.content,
|
||||
};
|
||||
WebSocketService.Instance.editPrivateMessage(editForm);
|
||||
} else {
|
||||
WebSocketService.Instance.createPrivateMessage(
|
||||
i.state.privateMessageForm
|
||||
);
|
||||
}
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleRecipientChange(i: PrivateMessageForm, event: any) {
|
||||
i.state.recipient = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleContentChange(i: PrivateMessageForm, event: any) {
|
||||
i.state.privateMessageForm.content = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleCancel(i: PrivateMessageForm) {
|
||||
i.props.onCancel();
|
||||
}
|
||||
|
||||
handlePreviewToggle(i: PrivateMessageForm, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.previewMode = !i.state.previewMode;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleShowDisclaimer(i: PrivateMessageForm) {
|
||||
i.state.showDisclaimer = !i.state.showDisclaimer;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
parseMessage(msg: any) {
|
||||
let op: UserOperation = msgOp(msg);
|
||||
if (msg.error) {
|
||||
alert(i18n.t(msg.error));
|
||||
this.state.loading = false;
|
||||
this.setState(this.state);
|
||||
return;
|
||||
} else if (op == UserOperation.EditPrivateMessage) {
|
||||
this.state.loading = false;
|
||||
let res: PrivateMessageResponse = msg;
|
||||
this.props.onEdit(res.message);
|
||||
} else if (op == UserOperation.GetUserDetails) {
|
||||
let res: UserDetailsResponse = msg;
|
||||
this.state.recipient = res.user;
|
||||
this.state.privateMessageForm.recipient_id = res.user.id;
|
||||
this.setState(this.state);
|
||||
} else if (op == UserOperation.CreatePrivateMessage) {
|
||||
this.state.loading = false;
|
||||
let res: PrivateMessageResponse = msg;
|
||||
this.props.onCreate(res.message);
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,249 @@
|
||||
import { Component, linkEvent } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import {
|
||||
PrivateMessage as PrivateMessageI,
|
||||
EditPrivateMessageForm,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { mdToHtml, pictshareAvatarThumbnail, showAvatars } from '../utils';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { PrivateMessageForm } from './private-message-form';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
interface PrivateMessageState {
|
||||
showReply: boolean;
|
||||
showEdit: boolean;
|
||||
collapsed: boolean;
|
||||
viewSource: boolean;
|
||||
}
|
||||
|
||||
interface PrivateMessageProps {
|
||||
privateMessage: PrivateMessageI;
|
||||
}
|
||||
|
||||
export class PrivateMessage extends Component<
|
||||
PrivateMessageProps,
|
||||
PrivateMessageState
|
||||
> {
|
||||
private emptyState: PrivateMessageState = {
|
||||
showReply: false,
|
||||
showEdit: false,
|
||||
collapsed: false,
|
||||
viewSource: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.state = this.emptyState;
|
||||
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
||||
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
|
||||
this
|
||||
);
|
||||
this.handlePrivateMessageEdit = this.handlePrivateMessageEdit.bind(this);
|
||||
}
|
||||
|
||||
get mine(): boolean {
|
||||
return UserService.Instance.user.id == this.props.privateMessage.creator_id;
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = this.props.privateMessage;
|
||||
return (
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<ul class="list-inline mb-0 text-muted small">
|
||||
<li className="list-inline-item">
|
||||
{this.mine ? i18n.t('to') : i18n.t('from')}
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
className="text-info"
|
||||
to={
|
||||
this.mine
|
||||
? `/u/${message.recipient_name}`
|
||||
: `/u/${message.creator_name}`
|
||||
}
|
||||
>
|
||||
{(this.mine
|
||||
? message.recipient_avatar
|
||||
: message.creator_avatar) &&
|
||||
showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictshareAvatarThumbnail(
|
||||
this.mine
|
||||
? message.recipient_avatar
|
||||
: message.creator_avatar
|
||||
)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{this.mine ? message.recipient_name : message.creator_name}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span>
|
||||
<MomentTime data={message} />
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<div
|
||||
className="pointer text-monospace"
|
||||
onClick={linkEvent(this, this.handleMessageCollapse)}
|
||||
>
|
||||
{this.state.collapsed ? '[+]' : '[-]'}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{this.state.showEdit && (
|
||||
<PrivateMessageForm
|
||||
privateMessage={message}
|
||||
onEdit={this.handlePrivateMessageEdit}
|
||||
onCancel={this.handleReplyCancel}
|
||||
/>
|
||||
)}
|
||||
{!this.state.showEdit && !this.state.collapsed && (
|
||||
<div>
|
||||
{this.state.viewSource ? (
|
||||
<pre>{this.messageUnlessRemoved}</pre>
|
||||
) : (
|
||||
<div
|
||||
className="md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
|
||||
/>
|
||||
)}
|
||||
<ul class="list-inline mb-1 text-muted small font-weight-bold">
|
||||
{!this.mine && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleMarkRead)}
|
||||
>
|
||||
{message.read
|
||||
? i18n.t('mark_as_unread')
|
||||
: i18n.t('mark_as_read')}
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleReplyClick)}
|
||||
>
|
||||
<T i18nKey="reply">#</T>
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{this.mine && (
|
||||
<>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleEditClick)}
|
||||
>
|
||||
<T i18nKey="edit">#</T>
|
||||
</span>
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
class="pointer"
|
||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||
>
|
||||
{!message.deleted
|
||||
? i18n.t('delete')
|
||||
: i18n.t('restore')}
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
<li className="list-inline-item">•</li>
|
||||
<li className="list-inline-item">
|
||||
<span
|
||||
className="pointer"
|
||||
onClick={linkEvent(this, this.handleViewSource)}
|
||||
>
|
||||
<T i18nKey="view_source">#</T>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{this.state.showReply && (
|
||||
<PrivateMessageForm
|
||||
params={{
|
||||
recipient_id: this.props.privateMessage.creator_id,
|
||||
}}
|
||||
onCreate={this.handlePrivateMessageCreate}
|
||||
/>
|
||||
)}
|
||||
{/* A collapsed clearfix */}
|
||||
{this.state.collapsed && <div class="row col-12"></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
get messageUnlessRemoved(): string {
|
||||
let message = this.props.privateMessage;
|
||||
return message.deleted ? `*${i18n.t('deleted')}*` : message.content;
|
||||
}
|
||||
|
||||
handleReplyClick(i: PrivateMessage) {
|
||||
i.state.showReply = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleEditClick(i: PrivateMessage) {
|
||||
i.state.showEdit = true;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleDeleteClick(i: PrivateMessage) {
|
||||
let form: EditPrivateMessageForm = {
|
||||
edit_id: i.props.privateMessage.id,
|
||||
deleted: !i.props.privateMessage.deleted,
|
||||
};
|
||||
WebSocketService.Instance.editPrivateMessage(form);
|
||||
}
|
||||
|
||||
handleReplyCancel() {
|
||||
this.state.showReply = false;
|
||||
this.state.showEdit = false;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleMarkRead(i: PrivateMessage) {
|
||||
let form: EditPrivateMessageForm = {
|
||||
edit_id: i.props.privateMessage.id,
|
||||
read: !i.props.privateMessage.read,
|
||||
};
|
||||
WebSocketService.Instance.editPrivateMessage(form);
|
||||
}
|
||||
|
||||
handleMessageCollapse(i: PrivateMessage) {
|
||||
i.state.collapsed = !i.state.collapsed;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleViewSource(i: PrivateMessage) {
|
||||
i.state.viewSource = !i.state.viewSource;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handlePrivateMessageEdit() {
|
||||
this.state.showEdit = false;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handlePrivateMessageCreate() {
|
||||
this.state.showReply = false;
|
||||
this.setState(this.state);
|
||||
alert(i18n.t('message_sent'));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue