import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; import { Subscription } from 'rxjs'; import { UserOperation, SortType, ListingType, SaveUserSettings, LoginResponse, DeleteAccount, GetSiteResponse, GetUserDetailsResponse, AddAdminResponse, GetUserDetails, CommentResponse, PostResponse, BanUserResponse, } from 'lemmy-js-client'; import { InitialFetchRequest, UserDetailsView } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { wsJsonToRes, fetchLimit, routeSortTypeToEnum, capitalizeFirstLetter, themes, setTheme, languages, toast, setupTippy, getLanguage, mdToHtml, elementUrl, setIsoData, getIdFromProps, getUsernameFromProps, wsSubscribe, createCommentLikeRes, editCommentRes, saveCommentRes, createPostLikeFindRes, previewLines, editPostFindRes, wsUserOp, wsClient, authField, setOptionalAuth, saveScrollPosition, restoreScrollPosition, } from '../utils'; import { UserListing } from './user-listing'; import { HtmlTags } from './html-tags'; import { SortSelect } from './sort-select'; import { ListingTypeSelect } from './listing-type-select'; import { MomentTime } from './moment-time'; import { i18n } from '../i18next'; import moment from 'moment'; import { UserDetails } from './user-details'; import { MarkdownTextArea } from './markdown-textarea'; import { ImageUploadForm } from './image-upload-form'; import { BannerIconHeader } from './banner-icon-header'; import { CommunityLink } from './community-link'; interface UserState { userRes: GetUserDetailsResponse; userId: number; userName: string; view: UserDetailsView; sort: SortType; page: number; loading: boolean; userSettingsForm: SaveUserSettings; userSettingsLoading: boolean; deleteAccountLoading: boolean; deleteAccountShowConfirm: boolean; deleteAccountForm: DeleteAccount; siteRes: GetSiteResponse; } interface UserProps { view: UserDetailsView; sort: SortType; page: number; user_id: number | null; username: string; } interface UrlParams { view?: string; sort?: SortType; page?: number; } export class User extends Component { private isoData = setIsoData(this.context); private subscription: Subscription; private emptyState: UserState = { userRes: undefined, userId: getIdFromProps(this.props), userName: getUsernameFromProps(this.props), loading: true, view: User.getViewFromProps(this.props.match.view), sort: User.getSortTypeFromProps(this.props.match.sort), page: User.getPageFromProps(this.props.match.page), userSettingsForm: { show_nsfw: null, theme: null, default_sort_type: null, default_listing_type: null, lang: null, show_avatars: null, send_notifications_to_email: null, bio: null, preferred_username: null, auth: authField(false), }, userSettingsLoading: null, deleteAccountLoading: null, deleteAccountShowConfirm: false, deleteAccountForm: { password: null, auth: authField(false), }, siteRes: this.isoData.site_res, }; constructor(props: any, context: any) { super(props, context); this.state = this.emptyState; this.handleSortChange = this.handleSortChange.bind(this); this.handleUserSettingsSortTypeChange = this.handleUserSettingsSortTypeChange.bind( this ); this.handleUserSettingsListingTypeChange = this.handleUserSettingsListingTypeChange.bind( this ); this.handlePageChange = this.handlePageChange.bind(this); this.handleUserSettingsBioChange = this.handleUserSettingsBioChange.bind( this ); this.handleAvatarUpload = this.handleAvatarUpload.bind(this); this.handleAvatarRemove = this.handleAvatarRemove.bind(this); this.handleBannerUpload = this.handleBannerUpload.bind(this); this.handleBannerRemove = this.handleBannerRemove.bind(this); this.parseMessage = this.parseMessage.bind(this); this.subscription = wsSubscribe(this.parseMessage); // Only fetch the data if coming from another route if (this.isoData.path == this.context.router.route.match.url) { this.state.userRes = this.isoData.routeData[0]; this.setUserInfo(); this.state.loading = false; } else { this.fetchUserData(); } setupTippy(); } fetchUserData() { let form: GetUserDetails = { user_id: this.state.userId, username: this.state.userName, sort: this.state.sort, saved_only: this.state.view === UserDetailsView.Saved, page: this.state.page, limit: fetchLimit, auth: authField(false), }; WebSocketService.Instance.send(wsClient.getUserDetails(form)); } get isCurrentUser() { return ( UserService.Instance.user && UserService.Instance.user.id == this.state.userRes.user_view.user.id ); } static getViewFromProps(view: string): UserDetailsView { return view ? UserDetailsView[view] : UserDetailsView.Overview; } static getSortTypeFromProps(sort: string): SortType { return sort ? routeSortTypeToEnum(sort) : SortType.New; } static getPageFromProps(page: number): number { return page ? Number(page) : 1; } static fetchInitialData(req: InitialFetchRequest): Promise[] { let pathSplit = req.path.split('/'); let promises: Promise[] = []; // It can be /u/me, or /username/1 let idOrName = pathSplit[2]; let user_id: number; let username: string; if (isNaN(Number(idOrName))) { username = idOrName; } else { user_id = Number(idOrName); } let view = this.getViewFromProps(pathSplit[4]); let sort = this.getSortTypeFromProps(pathSplit[6]); let page = this.getPageFromProps(Number(pathSplit[8])); let form: GetUserDetails = { sort, saved_only: view === UserDetailsView.Saved, page, limit: fetchLimit, }; setOptionalAuth(form, req.auth); this.setIdOrName(form, user_id, username); promises.push(req.client.getUserDetails(form)); return promises; } static setIdOrName(obj: any, id: number, name_: string) { if (id) { obj.user_id = id; } else { obj.username = name_; } } componentWillUnmount() { this.subscription.unsubscribe(); saveScrollPosition(this.context); } static getDerivedStateFromProps(props: any): UserProps { return { view: this.getViewFromProps(props.match.params.view), sort: this.getSortTypeFromProps(props.match.params.sort), page: this.getPageFromProps(props.match.params.page), user_id: Number(props.match.params.id) || null, username: props.match.params.username, }; } componentDidUpdate(lastProps: any, _lastState: UserState, _snapshot: any) { // Necessary if you are on a post and you click another post (same route) if ( lastProps.location.pathname.split('/')[2] !== lastProps.history.location.pathname.split('/')[2] ) { // Couldnt get a refresh working. This does for now. location.reload(); } } get documentTitle(): string { return `@${this.state.userRes.user_view.user.name} - ${this.state.siteRes.site_view.site.name}`; } get bioTag(): string { return this.state.userRes.user_view.user.bio ? previewLines(this.state.userRes.user_view.user.bio) : undefined; } render() { return (
{this.state.loading ? (
) : (
<> {this.userInfo()}
{!this.state.loading && this.selects()}
{!this.state.loading && (
{this.isCurrentUser && this.userSettings()} {this.moderates()} {this.follows()}
)}
)}
); } viewRadios() { return (
); } selects() { return (
{this.viewRadios()} #
); } userInfo() { let uv = this.state.userRes?.user_view; return (
{uv.user.preferred_username && (
{uv.user.preferred_username}
)}
  • {uv.user.banned && (
  • {i18n.t('banned')}
  • )}
{this.isCurrentUser ? ( ) : ( <> {i18n.t('send_secure_message')} {i18n.t('send_message')} )}
{uv.user.bio && (
)}
  • {i18n.t('number_of_posts', { count: uv.counts.post_count })}
  • {i18n.t('number_of_comments', { count: uv.counts.comment_count, })}
{i18n.t('joined')}{' '}
{i18n.t('cake_day_title')}{' '} {moment.utc(uv.user.published).local().format('MMM DD, YYYY')}
); } userSettings() { return (
{i18n.t('settings')}
0 } onChange={this.handleUserSettingsListingTypeChange} />
{this.state.siteRes.site_view.site.enable_nsfw && (
)}

{this.state.deleteAccountShowConfirm && ( <> )}
); } moderates() { return (
{this.state.userRes.moderates.length > 0 && (
{i18n.t('moderates')}
    {this.state.userRes.moderates.map(cmv => (
  • ))}
)}
); } follows() { return (
{this.state.userRes.follows.length > 0 && (
{i18n.t('subscribed')}
    {this.state.userRes.follows.map(cfv => (
  • ))}
)}
); } updateUrl(paramUpdates: UrlParams) { const page = paramUpdates.page || this.state.page; const viewStr = paramUpdates.view || UserDetailsView[this.state.view]; const sortStr = paramUpdates.sort || this.state.sort; let typeView = this.state.userName ? `/u/${this.state.userName}` : `/user/${this.state.userId}`; this.props.history.push( `${typeView}/view/${viewStr}/sort/${sortStr}/page/${page}` ); this.state.loading = true; this.setState(this.state); this.fetchUserData(); } handlePageChange(page: number) { this.updateUrl({ page }); } handleSortChange(val: SortType) { this.updateUrl({ sort: val, page: 1 }); } handleViewChange(i: User, event: any) { i.updateUrl({ view: UserDetailsView[Number(event.target.value)], page: 1, }); } handleUserSettingsShowNsfwChange(i: User, event: any) { i.state.userSettingsForm.show_nsfw = event.target.checked; i.setState(i.state); } handleUserSettingsShowAvatarsChange(i: User, event: any) { i.state.userSettingsForm.show_avatars = event.target.checked; UserService.Instance.user.show_avatars = event.target.checked; // Just for instant updates i.setState(i.state); } handleUserSettingsSendNotificationsToEmailChange(i: User, event: any) { i.state.userSettingsForm.send_notifications_to_email = event.target.checked; i.setState(i.state); } handleUserSettingsThemeChange(i: User, event: any) { i.state.userSettingsForm.theme = event.target.value; setTheme(event.target.value, true); i.setState(i.state); } handleUserSettingsLangChange(i: User, event: any) { i.state.userSettingsForm.lang = event.target.value; i18n.changeLanguage(getLanguage(i.state.userSettingsForm.lang)); i.setState(i.state); } handleUserSettingsSortTypeChange(val: SortType) { this.state.userSettingsForm.default_sort_type = Object.keys( SortType ).indexOf(val); this.setState(this.state); } handleUserSettingsListingTypeChange(val: ListingType) { this.state.userSettingsForm.default_listing_type = Object.keys( ListingType ).indexOf(val); this.setState(this.state); } handleUserSettingsEmailChange(i: User, event: any) { i.state.userSettingsForm.email = event.target.value; i.setState(i.state); } handleUserSettingsBioChange(val: string) { this.state.userSettingsForm.bio = val; this.setState(this.state); } handleAvatarUpload(url: string) { this.state.userSettingsForm.avatar = url; this.setState(this.state); } handleAvatarRemove() { this.state.userSettingsForm.avatar = ''; this.setState(this.state); } handleBannerUpload(url: string) { this.state.userSettingsForm.banner = url; this.setState(this.state); } handleBannerRemove() { this.state.userSettingsForm.banner = ''; this.setState(this.state); } handleUserSettingsPreferredUsernameChange(i: User, event: any) { i.state.userSettingsForm.preferred_username = event.target.value; i.setState(i.state); } handleUserSettingsMatrixUserIdChange(i: User, event: any) { i.state.userSettingsForm.matrix_user_id = event.target.value; if ( i.state.userSettingsForm.matrix_user_id == '' && !i.state.userRes.user_view.user.matrix_user_id ) { i.state.userSettingsForm.matrix_user_id = undefined; } i.setState(i.state); } handleUserSettingsNewPasswordChange(i: User, event: any) { i.state.userSettingsForm.new_password = event.target.value; if (i.state.userSettingsForm.new_password == '') { i.state.userSettingsForm.new_password = undefined; } i.setState(i.state); } handleUserSettingsNewPasswordVerifyChange(i: User, event: any) { i.state.userSettingsForm.new_password_verify = event.target.value; if (i.state.userSettingsForm.new_password_verify == '') { i.state.userSettingsForm.new_password_verify = undefined; } i.setState(i.state); } handleUserSettingsOldPasswordChange(i: User, event: any) { i.state.userSettingsForm.old_password = event.target.value; if (i.state.userSettingsForm.old_password == '') { i.state.userSettingsForm.old_password = undefined; } i.setState(i.state); } handleUserSettingsSubmit(i: User, event: any) { event.preventDefault(); i.state.userSettingsLoading = true; i.setState(i.state); WebSocketService.Instance.send( wsClient.saveUserSettings(i.state.userSettingsForm) ); } handleDeleteAccountShowConfirmToggle(i: User, event: any) { event.preventDefault(); i.state.deleteAccountShowConfirm = !i.state.deleteAccountShowConfirm; i.setState(i.state); } handleDeleteAccountPasswordChange(i: User, event: any) { i.state.deleteAccountForm.password = event.target.value; i.setState(i.state); } handleLogoutClick(i: User) { UserService.Instance.logout(); i.context.router.history.push('/'); } handleDeleteAccount(i: User, event: any) { event.preventDefault(); i.state.deleteAccountLoading = true; i.setState(i.state); WebSocketService.Instance.send( wsClient.deleteAccount(i.state.deleteAccountForm) ); i.handleLogoutClick(i); } setUserInfo() { if (this.isCurrentUser) { this.state.userSettingsForm.show_nsfw = UserService.Instance.user.show_nsfw; this.state.userSettingsForm.theme = UserService.Instance.user.theme ? UserService.Instance.user.theme : 'browser'; this.state.userSettingsForm.default_sort_type = UserService.Instance.user.default_sort_type; this.state.userSettingsForm.default_listing_type = UserService.Instance.user.default_listing_type; this.state.userSettingsForm.lang = UserService.Instance.user.lang; this.state.userSettingsForm.avatar = UserService.Instance.user.avatar; this.state.userSettingsForm.banner = UserService.Instance.user.banner; this.state.userSettingsForm.preferred_username = UserService.Instance.user.preferred_username; this.state.userSettingsForm.show_avatars = UserService.Instance.user.show_avatars; this.state.userSettingsForm.email = UserService.Instance.user.email; this.state.userSettingsForm.bio = UserService.Instance.user.bio; this.state.userSettingsForm.send_notifications_to_email = UserService.Instance.user.send_notifications_to_email; this.state.userSettingsForm.matrix_user_id = UserService.Instance.user.matrix_user_id; } } parseMessage(msg: any) { let op = wsUserOp(msg); if (msg.error) { toast(i18n.t(msg.error), 'danger'); if (msg.error == 'couldnt_find_that_username_or_email') { this.context.router.history.push('/'); } this.setState({ deleteAccountLoading: false, userSettingsLoading: false, }); return; } else if (msg.reconnect) { this.fetchUserData(); } else if (op == UserOperation.GetUserDetails) { // Since the UserDetails contains posts/comments as well as some general user info we listen here as well // and set the parent state if it is not set or differs // TODO this might need to get abstracted let data = wsJsonToRes(msg).data; this.state.userRes = data; this.setUserInfo(); this.state.loading = false; this.setState(this.state); restoreScrollPosition(this.context); } else if (op == UserOperation.SaveUserSettings) { let data = wsJsonToRes(msg).data; UserService.Instance.login(data); this.state.userRes.user_view.user.bio = this.state.userSettingsForm.bio; this.state.userRes.user_view.user.preferred_username = this.state.userSettingsForm.preferred_username; this.state.userRes.user_view.user.banner = this.state.userSettingsForm.banner; this.state.userRes.user_view.user.avatar = this.state.userSettingsForm.avatar; this.state.userSettingsLoading = false; this.setState(this.state); window.scrollTo(0, 0); } else if (op == UserOperation.DeleteAccount) { this.setState({ deleteAccountLoading: false, deleteAccountShowConfirm: false, }); this.context.router.history.push('/'); } else if (op == UserOperation.AddAdmin) { let data = wsJsonToRes(msg).data; this.state.siteRes.admins = data.admins; this.setState(this.state); } else if (op == UserOperation.CreateCommentLike) { let data = wsJsonToRes(msg).data; createCommentLikeRes(data.comment_view, this.state.userRes.comments); this.setState(this.state); } else if ( op == UserOperation.EditComment || op == UserOperation.DeleteComment || op == UserOperation.RemoveComment ) { let data = wsJsonToRes(msg).data; editCommentRes(data.comment_view, this.state.userRes.comments); this.setState(this.state); } else if (op == UserOperation.CreateComment) { let data = wsJsonToRes(msg).data; if ( UserService.Instance.user && data.comment_view.creator.id == UserService.Instance.user.id ) { toast(i18n.t('reply_sent')); } } else if (op == UserOperation.SaveComment) { let data = wsJsonToRes(msg).data; saveCommentRes(data.comment_view, this.state.userRes.comments); this.setState(this.state); } else if ( op == UserOperation.EditPost || op == UserOperation.DeletePost || op == UserOperation.RemovePost || op == UserOperation.LockPost || op == UserOperation.StickyPost || op == UserOperation.SavePost ) { let data = wsJsonToRes(msg).data; editPostFindRes(data.post_view, this.state.userRes.posts); this.setState(this.state); } else if (op == UserOperation.CreatePostLike) { let data = wsJsonToRes(msg).data; createPostLikeFindRes(data.post_view, this.state.userRes.posts); this.setState(this.state); } else if (op == UserOperation.BanUser) { let data = wsJsonToRes(msg).data; this.state.userRes.comments .filter(c => c.creator.id == data.user_view.user.id) .forEach(c => (c.creator.banned = data.banned)); this.state.userRes.posts .filter(c => c.creator.id == data.user_view.user.id) .forEach(c => (c.creator.banned = data.banned)); this.setState(this.state); } } }