import { Component, linkEvent } from 'inferno';
+import { Helmet } from 'inferno-helmet';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
SortType,
GetSiteResponse,
ListingType,
- DataType,
SiteResponse,
GetPostsResponse,
PostResponse,
AddAdminResponse,
BanUserResponse,
WebSocketJsonResponse,
-} from '../interfaces';
+} from 'lemmy-js-client';
+import { DataType } from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { PostListings } from './post-listings';
import { CommentNodes } from './comment-nodes';
import { ListingTypeSelect } from './listing-type-select';
import { DataTypeSelect } from './data-type-select';
import { SiteForm } from './site-form';
+import { UserListing } from './user-listing';
+import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
import {
wsJsonToRes,
repoUrl,
mdToHtml,
fetchLimit,
- pictshareAvatarThumbnail,
- showAvatars,
toast,
getListingTypeFromProps,
getPageFromProps,
createPostLikeFindRes,
editPostFindRes,
commentsToFlatNodes,
- commentSortSortType,
setupTippy,
+ favIconUrl,
+ notifyPost,
} from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
page: number;
}
+interface MainProps {
+ listingType: ListingType;
+ dataType: DataType;
+ sort: SortType;
+ page: number;
+}
+
+interface UrlParams {
+ listingType?: ListingType;
+ dataType?: string;
+ sort?: SortType;
+ page?: number;
+}
+
export class Main extends Component<any, MainState> {
private subscription: Subscription;
private emptyState: MainState = {
enable_downvotes: null,
open_registration: null,
enable_nsfw: null,
+ icon: null,
+ banner: null,
+ creator_preferred_username: null,
},
admins: [],
banned: [],
online: null,
+ version: null,
+ federated_instances: null,
},
showEditSite: false,
loading: true,
}
let listCommunitiesForm: ListCommunitiesForm = {
- sort: SortType[SortType.Hot],
+ sort: SortType.Hot,
limit: 6,
};
this.subscription.unsubscribe();
}
- // Necessary for back button for some reason
- componentWillReceiveProps(nextProps: any) {
+ static getDerivedStateFromProps(props: any): MainProps {
+ return {
+ listingType: getListingTypeFromProps(props),
+ dataType: getDataTypeFromProps(props),
+ sort: getSortTypeFromProps(props),
+ page: getPageFromProps(props),
+ };
+ }
+
+ componentDidUpdate(_: any, lastState: MainState) {
if (
- nextProps.history.action == 'POP' ||
- nextProps.history.action == 'PUSH'
+ lastState.listingType !== this.state.listingType ||
+ lastState.dataType !== this.state.dataType ||
+ lastState.sort !== this.state.sort ||
+ lastState.page !== this.state.page
) {
- this.state.listingType = getListingTypeFromProps(nextProps);
- this.state.dataType = getDataTypeFromProps(nextProps);
- this.state.sort = getSortTypeFromProps(nextProps);
- this.state.page = getPageFromProps(nextProps);
- this.setState(this.state);
+ this.setState({ loading: true });
this.fetchData();
}
}
+ get documentTitle(): string {
+ if (this.state.siteRes.site.name) {
+ return `${this.state.siteRes.site.name}`;
+ } else {
+ return 'Lemmy';
+ }
+ }
+
+ get favIcon(): string {
+ return this.state.siteRes.site.icon
+ ? this.state.siteRes.site.icon
+ : favIconUrl;
+ }
+
render() {
return (
<div class="container">
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
<div class="row">
<main role="main" class="col-12 col-md-8">
{this.posts()}
</main>
- <aside class="col-12 col-md-4">{this.my_sidebar()}</aside>
+ <aside class="col-12 col-md-4">{this.mySidebar()}</aside>
</div>
</div>
);
}
- my_sidebar() {
+ mySidebar() {
return (
<div>
{!this.state.loading && (
<div>
- <div class="card border-secondary mb-3">
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-header bg-transparent border-secondary">
+ <div class="mb-2">
+ {this.siteName()}
+ {this.adminButtons()}
+ </div>
+ <BannerIconHeader banner={this.state.siteRes.site.banner} />
+ </div>
<div class="card-body">
{this.trendingCommunities()}
- {UserService.Instance.user &&
- this.state.subscribedCommunities.length > 0 && (
- <div>
- <h5>
- <T i18nKey="subscribed_to_communities">
- #
- <Link class="text-body" to="/communities">
- #
- </Link>
- </T>
- </h5>
- <ul class="list-inline">
- {this.state.subscribedCommunities.map(community => (
- <li class="list-inline-item">
- <Link to={`/c/${community.community_name}`}>
- {community.community_name}
- </Link>
- </li>
- ))}
- </ul>
- </div>
- )}
- <Link
- class="btn btn-sm btn-secondary btn-block"
- to="/create_community"
- >
- {i18n.t('create_a_community')}
- </Link>
+ {this.createCommunityButton()}
+ {/*
+ {this.subscribedCommunities()}
+ */}
</div>
</div>
- {this.sidebar()}
- {this.landing()}
+
+ <div class="card bg-transparent border-secondary mb-3">
+ <div class="card-body">{this.sidebar()}</div>
+ </div>
+
+ <div class="card bg-transparent border-secondary">
+ <div class="card-body">{this.landing()}</div>
+ </div>
</div>
)}
</div>
);
}
+ createCommunityButton() {
+ return (
+ <Link class="btn btn-secondary btn-block" to="/create_community">
+ {i18n.t('create_a_community')}
+ </Link>
+ );
+ }
+
trendingCommunities() {
return (
<div>
<ul class="list-inline">
{this.state.trendingCommunities.map(community => (
<li class="list-inline-item">
- <Link to={`/c/${community.name}`}>{community.name}</Link>
+ <CommunityLink community={community} />
</li>
))}
</ul>
);
}
+ subscribedCommunities() {
+ return (
+ UserService.Instance.user &&
+ this.state.subscribedCommunities.length > 0 && (
+ <div>
+ <h5>
+ <T i18nKey="subscribed_to_communities">
+ #
+ <Link class="text-body" to="/communities">
+ #
+ </Link>
+ </T>
+ </h5>
+ <ul class="list-inline">
+ {this.state.subscribedCommunities.map(community => (
+ <li class="list-inline-item">
+ <CommunityLink
+ community={{
+ name: community.community_name,
+ id: community.community_id,
+ local: community.community_local,
+ actor_id: community.community_actor_id,
+ icon: community.community_icon,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ </div>
+ )
+ );
+ }
+
sidebar() {
return (
<div>
);
}
- updateUrl() {
- let listingTypeStr = ListingType[this.state.listingType].toLowerCase();
- let dataTypeStr = DataType[this.state.dataType].toLowerCase();
- let sortStr = SortType[this.state.sort].toLowerCase();
+ updateUrl(paramUpdates: UrlParams) {
+ const listingTypeStr = paramUpdates.listingType || this.state.listingType;
+ const dataTypeStr = paramUpdates.dataType || DataType[this.state.dataType];
+ const sortStr = paramUpdates.sort || this.state.sort;
+ const page = paramUpdates.page || this.state.page;
this.props.history.push(
- `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}`
+ `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${page}`
);
}
siteInfo() {
return (
<div>
- <div class="card border-secondary mb-3">
- <div class="card-body">
- <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
- {this.canAdmin && (
- <ul class="list-inline mb-1 text-muted font-weight-bold">
- <li className="list-inline-item-action">
- <span
- class="pointer"
- onClick={linkEvent(this, this.handleEditClick)}
- data-tippy-content={i18n.t('edit')}
- >
- <svg class="icon icon-inline">
- <use xlinkHref="#icon-edit"></use>
- </svg>
- </span>
- </li>
- </ul>
- )}
- <ul class="my-2 list-inline">
- <li className="list-inline-item badge badge-secondary">
- {i18n.t('number_online', { count: this.state.siteRes.online })}
- </li>
- <li className="list-inline-item badge badge-secondary">
- {i18n.t('number_of_users', {
- count: this.state.siteRes.site.number_of_users,
- })}
- </li>
- <li className="list-inline-item badge badge-secondary">
- {i18n.t('number_of_communities', {
- count: this.state.siteRes.site.number_of_communities,
- })}
- </li>
- <li className="list-inline-item badge badge-secondary">
- {i18n.t('number_of_posts', {
- count: this.state.siteRes.site.number_of_posts,
- })}
- </li>
- <li className="list-inline-item badge badge-secondary">
- {i18n.t('number_of_comments', {
- count: this.state.siteRes.site.number_of_comments,
- })}
- </li>
- <li className="list-inline-item">
- <Link className="badge badge-secondary" to="/modlog">
- {i18n.t('modlog')}
- </Link>
- </li>
- </ul>
- <ul class="mt-1 list-inline small mb-0">
- <li class="list-inline-item">{i18n.t('admins')}:</li>
- {this.state.siteRes.admins.map(admin => (
- <li class="list-inline-item">
- <Link
- class="text-body font-weight-bold"
- to={`/u/${admin.name}`}
- >
- {admin.avatar && showAvatars() && (
- <img
- height="32"
- width="32"
- src={pictshareAvatarThumbnail(admin.avatar)}
- class="rounded-circle mr-1"
- />
- )}
- <span>{admin.name}</span>
- </Link>
- </li>
- ))}
- </ul>
- </div>
- </div>
- {this.state.siteRes.site.description && (
- <div class="card border-secondary mb-3">
- <div class="card-body">
- <div
- className="md-div"
- dangerouslySetInnerHTML={mdToHtml(
- this.state.siteRes.site.description
- )}
- />
- </div>
- </div>
- )}
+ {this.state.siteRes.site.description && this.siteDescription()}
+ {this.badges()}
+ {this.admins()}
</div>
);
}
+ siteName() {
+ return <h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>;
+ }
+
+ admins() {
+ return (
+ <ul class="mt-1 list-inline small mb-0">
+ <li class="list-inline-item">{i18n.t('admins')}:</li>
+ {this.state.siteRes.admins.map(admin => (
+ <li class="list-inline-item">
+ <UserListing
+ user={{
+ name: admin.name,
+ preferred_username: admin.preferred_username,
+ avatar: admin.avatar,
+ local: admin.local,
+ actor_id: admin.actor_id,
+ id: admin.id,
+ }}
+ />
+ </li>
+ ))}
+ </ul>
+ );
+ }
+
+ badges() {
+ return (
+ <ul class="my-2 list-inline">
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_online', { count: this.state.siteRes.online })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_users', {
+ count: this.state.siteRes.site.number_of_users,
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_communities', {
+ count: this.state.siteRes.site.number_of_communities,
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_posts', {
+ count: this.state.siteRes.site.number_of_posts,
+ })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_comments', {
+ count: this.state.siteRes.site.number_of_comments,
+ })}
+ </li>
+ <li className="list-inline-item">
+ <Link className="badge badge-light" to="/modlog">
+ {i18n.t('modlog')}
+ </Link>
+ </li>
+ </ul>
+ );
+ }
+
+ adminButtons() {
+ return (
+ this.canAdmin && (
+ <ul class="list-inline mb-1 text-muted font-weight-bold">
+ <li className="list-inline-item-action">
+ <span
+ class="pointer"
+ onClick={linkEvent(this, this.handleEditClick)}
+ data-tippy-content={i18n.t('edit')}
+ >
+ <svg class="icon icon-inline">
+ <use xlinkHref="#icon-edit"></use>
+ </svg>
+ </span>
+ </li>
+ </ul>
+ )
+ );
+ }
+
+ siteDescription() {
+ return (
+ <div
+ className="md-div"
+ dangerouslySetInnerHTML={mdToHtml(this.state.siteRes.site.description)}
+ />
+ );
+ }
+
landing() {
return (
- <div class="card border-secondary">
- <div class="card-body">
- <h5>
- {i18n.t('powered_by')}
- <svg class="icon mx-2">
- <use xlinkHref="#icon-mouse">#</use>
- </svg>
- <a href={repoUrl}>
- Lemmy<sup>beta</sup>
+ <>
+ <h5>
+ {i18n.t('powered_by')}
+ <svg class="icon mx-2">
+ <use xlinkHref="#icon-mouse">#</use>
+ </svg>
+ <a href={repoUrl}>
+ Lemmy<sup>beta</sup>
+ </a>
+ </h5>
+ <p class="mb-0">
+ <T i18nKey="landing_0">
+ #
+ <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
+ #
</a>
- </h5>
- <p class="mb-0">
- <T i18nKey="landing_0">
+ <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
+ <br class="big"></br>
+ <code>#</code>
+ <br></br>
+ <b>#</b>
+ <br class="big"></br>
+ <a href={repoUrl}>#</a>
+ <br class="big"></br>
+ <a href="https://www.rust-lang.org">#</a>
+ <a href="https://actix.rs/">#</a>
+ <a href="https://infernojs.org">#</a>
+ <a href="https://www.typescriptlang.org/">#</a>
+ <br class="big"></br>
+ <a href="https://github.com/LemmyNet/lemmy/graphs/contributors?type=a">
#
- <a href="https://en.wikipedia.org/wiki/Social_network_aggregation">
- #
- </a>
- <a href="https://en.wikipedia.org/wiki/Fediverse">#</a>
- <br></br>
- <code>#</code>
- <br></br>
- <b>#</b>
- <br></br>
- <a href={repoUrl}>#</a>
- <br></br>
- <a href="https://www.rust-lang.org">#</a>
- <a href="https://actix.rs/">#</a>
- <a href="https://infernojs.org">#</a>
- <a href="https://www.typescriptlang.org/">#</a>
- </T>
- </p>
- </div>
- </div>
+ </a>
+ </T>
+ </p>
+ </>
);
}
posts() {
return (
<div class="main-content-wrapper">
- {this.selects()}
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
</h5>
) : (
<div>
+ {this.selects()}
{this.listings()}
{this.paginator()}
</div>
showCommunity
removeDuplicates
sort={this.state.sort}
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
+ enableNsfw={this.state.siteRes.site.enable_nsfw}
/>
) : (
<CommentNodes
noIndent
showCommunity
sortType={this.state.sort}
+ showContext
+ enableDownvotes={this.state.siteRes.site.enable_downvotes}
/>
);
}
<span class="mr-3">
<ListingTypeSelect
type_={this.state.listingType}
+ showLocal={
+ this.state.siteRes.federated_instances &&
+ this.state.siteRes.federated_instances.length > 0
+ }
onChange={this.handleListingTypeChange}
/>
</span>
</span>
{this.state.listingType == ListingType.All && (
<a
- href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
+ href={`/feeds/all.xml?sort=${this.state.sort}`}
target="_blank"
+ rel="noopener"
title="RSS"
>
<svg class="icon text-muted small">
{UserService.Instance.user &&
this.state.listingType == ListingType.Subscribed && (
<a
- href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
- SortType[this.state.sort]
- }`}
+ href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${this.state.sort}`}
target="_blank"
title="RSS"
+ rel="noopener"
>
<svg class="icon text-muted small">
<use xlinkHref="#icon-rss">#</use>
<div class="my-2">
{this.state.page > 1 && (
<button
- class="btn btn-sm btn-secondary mr-1"
+ class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
</button>
)}
- {this.state.posts.length == fetchLimit && (
+ {this.state.posts.length > 0 && (
<button
- class="btn btn-sm btn-secondary"
+ class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
}
nextPage(i: Main) {
- i.state.page++;
- i.state.loading = true;
- i.setState(i.state);
- i.updateUrl();
- i.fetchData();
+ i.updateUrl({ page: i.state.page + 1 });
window.scrollTo(0, 0);
}
prevPage(i: Main) {
- i.state.page--;
- i.state.loading = true;
- i.setState(i.state);
- i.updateUrl();
- i.fetchData();
+ i.updateUrl({ page: i.state.page - 1 });
window.scrollTo(0, 0);
}
handleSortChange(val: SortType) {
- this.state.sort = val;
- this.state.page = 1;
- this.state.loading = true;
- this.setState(this.state);
- this.updateUrl();
- this.fetchData();
+ this.updateUrl({ sort: val, page: 1 });
window.scrollTo(0, 0);
}
handleListingTypeChange(val: ListingType) {
- this.state.listingType = val;
- this.state.page = 1;
- this.state.loading = true;
- this.setState(this.state);
- this.updateUrl();
- this.fetchData();
+ this.updateUrl({ listingType: val, page: 1 });
window.scrollTo(0, 0);
}
handleDataTypeChange(val: DataType) {
- this.state.dataType = val;
- this.state.page = 1;
- this.state.loading = true;
- this.setState(this.state);
- this.updateUrl();
- this.fetchData();
+ this.updateUrl({ dataType: DataType[val], page: 1 });
window.scrollTo(0, 0);
}
let getPostsForm: GetPostsForm = {
page: this.state.page,
limit: fetchLimit,
- sort: SortType[this.state.sort],
- type_: ListingType[this.state.listingType],
+ sort: this.state.sort,
+ type_: this.state.listingType,
};
WebSocketService.Instance.getPosts(getPostsForm);
} else {
let getCommentsForm: GetCommentsForm = {
page: this.state.page,
limit: fetchLimit,
- sort: SortType[this.state.sort],
- type_: ListingType[this.state.listingType],
+ sort: this.state.sort,
+ type_: this.state.listingType,
};
WebSocketService.Instance.getComments(getCommentsForm);
}
this.state.siteRes.banned = data.banned;
this.state.siteRes.online = data.online;
this.setState(this.state);
- document.title = `${WebSocketService.Instance.site.name}`;
} else if (res.op == UserOperation.EditSite) {
let data = res.data as SiteResponse;
this.state.siteRes.site = data.site;
this.state.showEditSite = false;
this.setState(this.state);
+ toast(i18n.t('site_saved'));
} else if (res.op == UserOperation.GetPosts) {
let data = res.data as GetPostsResponse;
this.state.posts = data.posts;
.includes(data.post.community_id)
) {
this.state.posts.unshift(data.post);
+ notifyPost(data.post, this.context.router);
}
} else {
// NSFW posts
UserService.Instance.user.show_nsfw)
) {
this.state.posts.unshift(data.post);
- this.setState(this.state);
+ notifyPost(data.post, this.context.router);
}
}
+ this.setState(this.state);
} else if (res.op == UserOperation.EditPost) {
let data = res.data as PostResponse;
editPostFindRes(data, this.state.posts);
this.state.comments = data.comments;
this.state.loading = false;
this.setState(this.state);
- } else if (res.op == UserOperation.EditComment) {
+ } else if (
+ res.op == UserOperation.EditComment ||
+ res.op == UserOperation.DeleteComment ||
+ res.op == UserOperation.RemoveComment
+ ) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState(this.state);