`, and ``.
-$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
-$font-family-base: $font-family-sans-serif;
-
-$baseWidth: 10px;
-$font-size-base: 18px;
-$font-size-large: $font-size-base;
-$font-size-small: $font-size-base;
-
-$font-size-h1: $font-size-base;
-$font-size-h2: $font-size-base;
-$font-size-h3: $font-size-base;
-$font-size-h4: $font-size-base;
-$font-size-h5: $font-size-base;
-$font-size-h6: $font-size-base;
-
-//** Unit-less `line-height` for use in components like buttons.
-$baseLineHeight: 19px;
-$line-height-base: $baseLineHeight;
-//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
-$line-height-computed: $line-height-base;
-
-//** By default, this inherits from the ``.
-$headings-font-family: inherit;
-$headings-font-weight: normal;
-$headings-line-height: $line-height-base;
-$headings-color: inherit;
-
-$space: $baseWidth;
-$halfbaseLineHeight: ($baseLineHeight / 2);
-$borderWidth: 2px;
-$baseLineWidth: ($baseLineHeight / 2);
-$halfSpace: ($baseWidth / 2);
-$lhsNB: ($baseWidth / 2 + 1);
-$rhsNB: ($baseWidth / 2 - 1);
-$lhs: ($lhsNB - ($borderWidth));
-$rhs: ($rhsNB - ($borderWidth / 2));
-$tsNB: ($baseLineHeight / 2);
-$bsNB: $tsNB;
-$ts: ($tsNB - ($borderWidth / 2));
-$bs: $ts;
-$tsMargin: 3px;
-
-//== Iconography
-//
-//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
-
-//** Load fonts from this directory.
-$icon-font-path: "../fonts/";
-//** File name for all font files.
-$icon-font-name: "glyphicons-halflings-regular";
-//** Element ID within SVG icon file.
-$icon-font-svg-id: "glyphicons_halflingsregular";
-
-//== Components
-//
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-
-$padding-base-vertical: 0px;
-$padding-base-horizontal: 0px;
-
-$padding-large-vertical: 0px;
-$padding-large-horizontal: $halfSpace;
-
-$padding-small-vertical: 0px;
-$padding-small-horizontal: 0px;
-
-$padding-xs-vertical: 0px;
-$padding-xs-horizontal: 0px;
-
-$line-height-large: $baseLineHeight;
-$line-height-small: $baseLineHeight;
-
-$border-radius-base: 0;
-$border-radius-large: 0;
-$border-radius-small: 0;
-
-//** Global color for active items (e.g., navs or dropdowns).
-$component-active-color: $white;
-//** Global background color for active items (e.g., navs or dropdowns).
-$component-active-bg: $black;
-
-//** Width of the `border` for generating carets that indicator dropdowns.
-$caret-width-base: 4px;
-//** Carets increase slightly in size for larger components.
-$caret-width-large: 5px;
-
-//== Tables
-//
-//## Customizes the `.table` component with basic values, each used across all table variations.
-
-//** Padding for ``s and ` `s.
-$table-cell-padding: $ts $rhs $bs $lhs;
-//** Padding for cells in `.table-condensed`.
-$table-condensed-cell-padding: $ts $rhs $bs $lhs;
-
-//** Default background color used for all tables.
-$table-bg: transparent;
-//** Background color used for `.table-striped`.
-$table-bg-accent: $black;
-//** Background color used for `.table-hover`.
-$table-bg-hover: #f5f5f5;
-$table-bg-active: $table-bg-hover;
-
-//** Border color for table and cell borders.
-$table-border-color: $gray;
-
-//== Buttons
-//
-//## For each of Bootstrap's buttons, define text, background and border color.
-
-$btn-font-weight: normal;
-
-$btn-default-color: $black;
-$btn-default-bg: $grayLight;
-$btn-default-border: $grayLight;
-
-$btn-primary-color: $black;
-$btn-primary-bg: $cyanDark;
-$btn-primary-border: $grayLight;
-
-$btn-success-color: #fff;
-$btn-success-bg: $brand-success;
-$btn-success-border: $btn-success-bg;
-
-$btn-info-color: #fff;
-$btn-info-bg: $brand-info;
-$btn-info-border: $btn-info-bg;
-
-$btn-warning-color: #fff;
-$btn-warning-bg: $brand-warning;
-$btn-warning-border: $btn-warning-bg;
-
-$btn-danger-color: #fff;
-$btn-danger-bg: $brand-danger;
-$btn-danger-border: $btn-danger-bg;
-
-$btn-link-disabled-color: $gray-light;
-
-//== Forms
-//
-//##
-
-//** ` ` background color
-$input-bg: $cyanDark;
-//** ` ` background color
-$input-bg-disabled: $gray-lighter;
-
-//** Text color for ` `s
-$input-color: $white;
-//** ` ` border color
-$input-border: #ccc;
-
-// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
-//** Default `.form-control` border radius
-// This has no effect on ``s in some browsers, due to the limited stylability of ``s in CSS.
-$input-border-radius: $border-radius-base;
-//** Large `.form-control` border radius
-$input-border-radius-large: $border-radius-large;
-//** Small `.form-control` border radius
-$input-border-radius-small: $border-radius-small;
-
-//** Border color for inputs on focus
-$input-border-focus: $black;
-
-//** Placeholder text color
-$input-color-placeholder: $black;
-
-//** Default `.form-control` height
-$input-height-base: $line-height-computed;
-//** Large `.form-control` height
-$input-height-large: $input-height-base;
-//** Small `.form-control` height
-$input-height-small: $input-height-base;
-
-$legend-color: $gray-dark;
-$legend-border-color: #e5e5e5;
-
-//** Background color for textual input addons
-$input-group-addon-bg: $gray-lighter;
-//** Border color for textual input addons
-$input-group-addon-border-color: $input-border;
-
-//** Disabled cursor for form controls and buttons.
-$cursor-disabled: not-allowed;
-
-//== Dropdowns
-//
-//## Dropdown menu container and contents.
-
-//** Background for the dropdown menu.
-$dropdown-bg: $gray;
-//** Dropdown menu `border-color`.
-$dropdown-border: rgb(0, 0, 0);
-//** Dropdown menu `border-color` **for IE8**.
-$dropdown-fallback-border: #ccc;
-//** Divider color for between dropdown items.
-$dropdown-divider-bg: $black;
-
-//** Dropdown link text color.
-$dropdown-link-color: $black;
-//** Hover color for dropdown links.
-$dropdown-link-hover-color: $gray;
-//** Hover background for dropdown links.
-$dropdown-link-hover-bg: $black;
-
-//** Active dropdown menu item text color.
-$dropdown-link-active-color: $component-active-color;
-//** Active dropdown menu item background color.
-$dropdown-link-active-bg: $component-active-bg;
-
-//** Disabled dropdown menu item background color.
-$dropdown-link-disabled-color: $gray-light;
-
-//** Text color for headers within dropdown menus.
-$dropdown-header-color: $black;
-
-//** Deprecated `$dropdown-caret-color` as of v3.1.0
-$dropdown-caret-color: #000;
-
-//-- Z-index master list
-//
-// Warning: Avoid customizing these values. They're used for a bird's eye view
-// of components dependent on the z-axis and are designed to all work together.
-//
-// Note: These variables are not generated into the Customizer.
-
-$zindex-navbar: 1000;
-$zindex-dropdown: 1000;
-$zindex-popover: 1060;
-$zindex-tooltip: 1070;
-$zindex-navbar-fixed: 1030;
-$zindex-modal: 1040;
-
-//== Media queries breakpoints
-//
-//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
-
-// Extra small screen / phone
-//** Deprecated `$screen-xs` as of v3.0.1
-$screen-xs: 480px;
-//** Deprecated `$screen-xs-min` as of v3.2.0
-$screen-xs-min: $screen-xs;
-//** Deprecated `$screen-phone` as of v3.0.1
-$screen-phone: $screen-xs-min;
-
-// Small screen / tablet
-//** Deprecated `$screen-sm` as of v3.0.1
-$screen-sm: 768px;
-$screen-sm-min: $screen-sm;
-//** Deprecated `$screen-tablet` as of v3.0.1
-$screen-tablet: $screen-sm-min;
-
-// Medium screen / desktop
-//** Deprecated `$screen-md` as of v3.0.1
-$screen-md: 992px;
-$screen-md-min: $screen-md;
-//** Deprecated `$screen-desktop` as of v3.0.1
-$screen-desktop: $screen-md-min;
-
-// Large screen / wide desktop
-//** Deprecated `$screen-lg` as of v3.0.1
-$screen-lg: 1200px;
-$screen-lg-min: $screen-lg;
-//** Deprecated `$screen-lg-desktop` as of v3.0.1
-$screen-lg-desktop: $screen-lg-min;
-
-// So media queries don't overlap when required, provide a maximum
-$screen-xs-max: ($screen-sm-min - 1);
-$screen-sm-max: ($screen-md-min - 1);
-$screen-md-max: ($screen-lg-min - 1);
-
-//== Grid system
-//
-//## Define your custom responsive grid.
-
-//** Number of columns in the grid.
-$grid-columns: 12;
-//** Padding between columns. Gets divided in half for the left and right.
-$grid-gutter-width: ($baseWidth * 2);
-// Navbar collapse
-//** Point at which the navbar becomes uncollapsed.
-$grid-float-breakpoint: $screen-sm-min;
-//** Point at which the navbar begins collapsing.
-$grid-float-breakpoint-max: ($grid-float-breakpoint);
-
-//== Container sizes
-//
-//## Define the maximum width of `.container` for different screen sizes.
-
-// Small screen / tablet
-$container-tablet: (720px + $grid-gutter-width);
-//** For `$screen-sm-min` and up.
-$container-sm: $container-tablet;
-
-// Medium screen / desktop
-$container-desktop: (940px + $grid-gutter-width);
-//** For `$screen-md-min` and up.
-$container-md: $container-desktop;
-
-// Large screen / wide desktop
-$container-large-desktop: (1140px + $grid-gutter-width);
-//** For `$screen-lg-min` and up.
-$container-lg: $container-large-desktop;
-
-//== Navbar
-//
-//##
-
-// Basics of a navbar
-$navbar-height: 0px;
-$navbar-margin-bottom: $line-height-computed;
-$navbar-border-radius: $border-radius-base;
-$navbar-padding-horizontal: ($baseWidth * 2);
-$navbar-padding-vertical: 0;
-$navbar-collapse-max-height: 340px;
-
-$navbar-default-color: $black;
-$navbar-default-bg: $grayLight;
-$navbar-default-border: $navbar-default-bg;
-
-// Navbar links
-$navbar-default-link-color: $black;
-$navbar-default-link-hover-color: $white;
-$navbar-default-link-hover-bg: $black;
-$navbar-default-link-active-color: $white;
-$navbar-default-link-active-bg: $black;
-$navbar-default-link-disabled-color: $gray;
-$navbar-default-link-disabled-bg: transparent;
-
-// Navbar brand label
-$navbar-default-brand-color: $navbar-default-link-color;
-$navbar-default-brand-hover-color: $navbar-default-brand-color;
-$navbar-default-brand-hover-bg: transparent;
-
-// Navbar toggle
-$navbar-default-toggle-hover-bg: #ddd;
-$navbar-default-toggle-icon-bar-bg: #888;
-$navbar-default-toggle-border-color: #ddd;
-
-// Inverted navbar
-// Reset inverted navbar basics
-$navbar-inverse-color: $gray;
-$navbar-inverse-bg: $black;
-$navbar-inverse-border: $navbar-inverse-bg;
-
-// Inverted navbar links
-$navbar-inverse-link-color: $gray-light;
-$navbar-inverse-link-hover-color: $black;
-$navbar-inverse-link-hover-bg: $grayLight;
-$navbar-inverse-link-active-color: $white;
-$navbar-inverse-link-active-bg: $grayDark;
-$navbar-inverse-link-disabled-color: $gray;
-$navbar-inverse-link-disabled-bg: transparent;
-
-// Inverted navbar brand label
-$navbar-inverse-brand-color: $navbar-inverse-link-color;
-$navbar-inverse-brand-hover-color: #fff;
-$navbar-inverse-brand-hover-bg: transparent;
-
-// Inverted navbar toggle
-$navbar-inverse-toggle-hover-bg: $grayLight;
-$navbar-inverse-toggle-icon-bar-bg: #fff;
-$navbar-inverse-toggle-border-color: #333;
-
-//== Navs
-//
-//##
-
-//=== Shared nav styles
-$nav-link-padding: 0 $baseWidth;
-$nav-link-hover-bg: $gray-lighter;
-
-$nav-disabled-link-color: $gray-light;
-$nav-disabled-link-hover-color: $gray-light;
-
-//== Tabs
-$nav-tabs-border-color: #ddd;
-
-$nav-tabs-link-hover-border-color: $gray-lighter;
-
-$nav-tabs-active-link-hover-bg: $black;
-$nav-tabs-active-link-hover-color: $white;
-
-$nav-tabs-justified-active-link-border-color: $body-bg;
-
-//== Pills
-$nav-pills-border-radius: $border-radius-base;
-$nav-pills-active-link-hover-bg: $component-active-bg;
-$nav-pills-active-link-hover-color: $component-active-color;
-
-//== Pagination
-//
-//##
-
-$pagination-color: $black;
-$pagination-bg: $gray;
-$pagination-border: #ddd;
-
-$pagination-hover-color: $link-hover-color;
-$pagination-hover-bg: $gray-lighter;
-$pagination-hover-border: #ddd;
-
-$pagination-active-color: #fff;
-$pagination-active-bg: $brand-primary;
-$pagination-active-border: $brand-primary;
-
-$pagination-disabled-color: $gray-light;
-$pagination-disabled-bg: #fff;
-$pagination-disabled-border: #ddd;
-
-//== Pager
-//
-//##
-
-$pager-bg: $pagination-bg;
-$pager-border: $pagination-border;
-$pager-border-radius: 0;
-
-$pager-hover-bg: $pagination-hover-bg;
-
-$pager-active-bg: $pagination-active-bg;
-$pager-active-color: $pagination-active-color;
-
-$pager-disabled-color: $pagination-disabled-color;
-
-//== Jumbotron
-//
-//##
-
-$jumbotron-padding: ($ts) ($rhs + $baseWidth) ($bs) ($lhs + $baseWidth);
-$jumbotron-color: $white;
-$jumbotron-bg: transparent;
-$jumbotron-heading-color: inherit;
-$jumbotron-font-size: $font-size-base;
-
-//== Form states and alerts
-//
-//## Define colors for form feedback states and, by default, alerts.
-
-$state-success-text: $green;
-$state-success-bg: $greenDark;
-$state-success-border: $state-success-bg;
-
-$state-info-text: $yellow;
-$state-info-bg: $brown;
-$state-info-border: $state-info-bg;
-
-$state-warning-text: $magenta;
-$state-warning-bg: $magentaDark;
-$state-warning-border: $state-warning-bg;
-
-$state-danger-text: $red;
-$state-danger-bg: $black;
-$state-danger-border: $state-danger-bg;
-
-//== Tooltips
-//
-//##
-
-//** Tooltip max width
-$tooltip-max-width: ($baseWidth * 25);
-//** Tooltip text color
-$tooltip-color: $white;
-//** Tooltip background color
-$tooltip-bg: $grayDark;
-$tooltip-opacity: 1;
-
-//** Tooltip arrow width
-$tooltip-arrow-width: 0px;
-//** Tooltip arrow color
-$tooltip-arrow-color: $tooltip-bg;
-
-//== Popovers
-//
-//##
-
-//** Popover body background color
-$popover-bg: $gray;
-//** Popover maximum width
-$popover-max-width: ($baseWidth * 20);
-//** Popover border color
-$popover-border-color: rgb(0, 0, 0);
-//** Popover fallback border color
-$popover-fallback-border-color: #ccc;
-
-//** Popover title background color
-$popover-title-bg: $greenDark;
-
-//** Popover arrow width
-$popover-arrow-width: 10px;
-//** Popover arrow color
-$popover-arrow-color: $popover-bg;
-
-//** Popover outer arrow width
-$popover-arrow-outer-width: ($popover-arrow-width + 1);
-//** Popover outer arrow color
-$popover-arrow-outer-color: $popover-border-color;
-//** Popover outer arrow fallback color
-$popover-arrow-outer-fallback-color: $popover-fallback-border-color;
-
-//== Labels
-//
-//##
-
-//** Default label background color
-$label-default-bg: $gray-light;
-//** Primary label background color
-$label-primary-bg: $brand-primary-bg;
-//** Success label background color
-$label-success-bg: $brand-success;
-//** Info label background color
-$label-info-bg: $brand-info;
-//** Warning label background color
-$label-warning-bg: $brand-warning;
-//** Danger label background color
-$label-danger-bg: $brand-danger;
-
-//** Default label text color
-$label-color: #fff;
-//** Default text color of a linked label
-$label-link-hover-color: #fff;
-
-//== Modals
-//
-//##
-
-//** Padding applied to the modal body
-$modal-inner-padding: 0 $baseWidth;
-
-//** Padding applied to the modal title
-$modal-title-padding: 0 $baseWidth;
-//** Modal title line-height
-$modal-title-line-height: $line-height-base;
-
-//** Background color of modal content area
-$modal-content-bg: $gray;
-//** Modal content border color
-$modal-content-border-color: rgb(0, 0, 0);
-//** Modal content border color **for IE8**
-$modal-content-fallback-border-color: #999;
-
-//** Modal backdrop background color
-$modal-backdrop-bg: #000;
-//** Modal backdrop opacity
-// $modal-backdrop-opacity: @include 5;
-//** Modal header border color
-$modal-header-border-color: #e5e5e5;
-//** Modal footer border color
-$modal-footer-border-color: $modal-header-border-color;
-
-$modal-lg: 900px;
-$modal-md: 600px;
-$modal-sm: 300px;
-
-//== Alerts
-//
-//## Define alert colors, border radius, and padding.
-
-$alert-padding: $line-height-base ($baseWidth * 2);
-$alert-border-radius: $border-radius-base;
-$alert-link-font-weight: normal;
-
-$alert-success-bg: $state-success-bg;
-$alert-success-text: $state-success-text;
-$alert-success-border: $state-success-border;
-
-$alert-info-bg: $state-info-bg;
-$alert-info-text: $state-info-text;
-$alert-info-border: $state-info-border;
-
-$alert-warning-bg: $state-warning-bg;
-$alert-warning-text: $state-warning-text;
-$alert-warning-border: $state-warning-border;
-
-$alert-danger-bg: $state-danger-bg;
-$alert-danger-text: $state-danger-text;
-$alert-danger-border: $state-danger-border;
-
-//== Progress bars
-//
-//##
-
-//** Background color of the whole progress component
-$progress-bg: $black;
-//** Progress bar text color
-$progress-bar-color: $black;
-//** Variable for setting rounded corners on progress bar.
-$progress-border-radius: $border-radius-base;
-
-//** Default progress bar color
-$progress-bar-bg: $brand-primary;
-//** Success progress bar color
-$progress-bar-success-bg: $brand-success;
-//** Warning progress bar color
-$progress-bar-warning-bg: $brand-warning;
-//** Danger progress bar color
-$progress-bar-danger-bg: $brand-danger;
-//** Info progress bar color
-$progress-bar-info-bg: $brand-info;
-
-//== List group
-//
-//##
-
-//** Background color on `.list-group-item`
-$list-group-bg: $gray;
-//** `.list-group-item` border color
-$list-group-border: #ddd;
-//** List group border radius
-$list-group-border-radius: $border-radius-base;
-
-//** Background color of single list items on hover
-$list-group-hover-bg: $black;
-//** Text color of active list items
-$list-group-active-color: $component-active-color;
-//** Background color of active list items
-$list-group-active-bg: $component-active-bg;
-//** Border color of active list elements
-$list-group-active-border: $list-group-active-bg;
-//** Text color for content within active list items
-$list-group-active-text-color: $component-active-color;
-
-//** Text color of disabled list items
-$list-group-disabled-color: $gray-dark;
-//** Background color of disabled list items
-$list-group-disabled-bg: $gray-lighter;
-//** Text color for content within disabled list items
-$list-group-disabled-text-color: $list-group-disabled-color;
-
-$list-group-link-color: $black;
-$list-group-link-hover-color: $list-group-link-color;
-$list-group-link-heading-color: #333;
-
-//== Panels
-//
-//##
-
-$panel-bg: $gray;
-$panel-body-padding: 0 $rhsNB 0 $lhsNB;
-$panel-heading-padding: 0 $rhsNB 0 $lhsNB;
-$panel-footer-padding: $panel-heading-padding;
-$panel-border-radius: $border-radius-base;
-
-//** Border color for elements within panels
-$panel-inner-border: #ddd;
-$panel-footer-bg: #f5f5f5;
-
-$panel-default-text: $white;
-$panel-default-border: #ddd;
-$panel-default-heading-bg: $grayDark;
-
-$panel-primary-text: $white;
-$panel-primary-border: $brand-primary;
-$panel-primary-heading-bg: $cyanDark;
-
-$panel-success-text: $state-success-text;
-$panel-success-border: $state-success-border;
-$panel-success-heading-bg: $state-success-bg;
-
-$panel-info-text: $state-info-text;
-$panel-info-border: $state-info-border;
-$panel-info-heading-bg: $state-info-bg;
-
-$panel-warning-text: $state-warning-text;
-$panel-warning-border: $state-warning-border;
-$panel-warning-heading-bg: $state-warning-bg;
-
-$panel-danger-text: $state-danger-text;
-$panel-danger-border: $state-danger-border;
-$panel-danger-heading-bg: $state-danger-bg;
-
-//== Thumbnails
-//
-//##
-
-//** Padding around the thumbnail image
-$thumbnail-padding: 4px;
-//** Thumbnail background color
-$thumbnail-bg: $body-bg;
-//** Thumbnail border color
-$thumbnail-border: #ddd;
-//** Thumbnail border radius
-$thumbnail-border-radius: $border-radius-base;
-
-//** Custom text color for thumbnail captions
-$thumbnail-caption-color: $text-color;
-//** Padding around the thumbnail caption
-$thumbnail-caption-padding: 9px;
-
-//== Wells
-//
-//##
-
-$well-bg: $greenDark;
-$well-border: $well-bg;
-
-//== Badges
-//
-//##
-
-$badge-color: $black;
-//** Linked badge text color on hover
-$badge-link-hover-color: #fff;
-$badge-bg: $gray-light;
-
-//** Badge text color in active nav link
-$badge-active-color: $link-color;
-//** Badge background color in active nav link
-$badge-active-bg: $black;
-
-$badge-font-weight: normal;
-$badge-line-height: $line-height-base;
-$badge-border-radius: 0;
-
-//== Breadcrumbs
-//
-//##
-
-$breadcrumb-padding-vertical: 8px;
-$breadcrumb-padding-horizontal: 15px;
-//** Breadcrumb background color
-$breadcrumb-bg: #f5f5f5;
-//** Breadcrumb text color
-$breadcrumb-color: #ccc;
-//** Text color of current page in the breadcrumb
-$breadcrumb-active-color: $gray-light;
-//** Textual separator for between breadcrumb elements
-$breadcrumb-separator: "/";
-
-//== Carousel
-//
-//##
-
-$carousel-text-shadow: none;
-
-$carousel-control-color: #fff;
-$carousel-control-width: 15%;
-$carousel-control-opacity: 1;
-$carousel-control-font-size: $font-size-base;
-
-$carousel-indicator-active-bg: #fff;
-$carousel-indicator-border-color: #fff;
-
-$carousel-caption-color: #fff;
-
-//== Close
-//
-//##
-
-$close-font-weight: normal;
-$close-color: #000;
-$close-text-shadow: none;
-
-//== Code
-//
-//##
-
-$code-color: #c7254e;
-$code-bg: #f9f2f4;
-
-$kbd-color: #fff;
-$kbd-bg: #333;
-
-$pre-bg: #f5f5f5;
-$pre-color: $gray-dark;
-$pre-border-color: #ccc;
-$pre-scrollable-max-height: 340px;
-
-//== Type
-//
-//##
-
-//** Horizontal offset for forms and lists.
-$component-offset-horizontal: 180px;
-//** Text muted color
-$text-muted: $gray-dark;
-//** Abbreviations and acronyms border color
-$abbr-border-color: $gray-light;
-//** Headings small color
-$headings-small-color: $gray-light;
-//** Blockquote small color
-$blockquote-small-color: $gray-light;
-//** Blockquote font size
-$blockquote-font-size: $font-size-base;
-//** Blockquote border color
-$blockquote-border-color: $gray-lighter;
-//** Page header border color
-$page-header-border-color: $gray-lighter;
-//** Width of horizontal description list titles
-$dl-horizontal-offset: $component-offset-horizontal;
-//** Horizontal line color.
-$hr-border: $black;
diff --git a/src/assets/css/themes/_variables.i386.scss b/src/assets/css/themes/_variables.i386.scss
deleted file mode 100644
index 259646fd..00000000
--- a/src/assets/css/themes/_variables.i386.scss
+++ /dev/null
@@ -1,39 +0,0 @@
-$blue: #5555ff;
-$cyan: #55ffff;
-$green: #55ff55;
-$indigo: #ff55ff;
-$red: #ff5555;
-$yellow: #fefe54;
-$orange: #a85400;
-$pink: #fe54fe;
-$purple: #fe5454;
-$primary: #fefe54;
-$body-bg: #000084;
-$gray-300: #bbb;
-$body-color: $gray-300;
-$link-hover-color: $white;
-$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
-$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
-$navbar-dark-color: $gray-300;
-$navbar-light-brand-color: $gray-300;
-$success: #00aa00;
-$danger: #aa0000;
-$info: #00aaaa;
-$warning: #aa00aa;
-$navbar-dark-active-color: $gray-100;
-$enable-rounded: false;
-$input-color: $white;
-$input-bg: rgb(102, 102, 102);
-$input-disabled-bg: $gray-800;
-$nav-tabs-link-active-color: $gray-100;
-$navbar-dark-hover-color: rgba($gray-300, 0.75);
-$light: $gray-800;
-$navbar-light-disabled-color: $gray-800;
-$navbar-light-active-color: $gray-100;
-$navbar-light-hover-color: $gray-200;
-$navbar-light-color: $gray-300;
-$card-bg: $gray-800;
-$card-border-color: $white;
-$input-placeholder-color: $gray-500;
-$mark-bg: #463b00;
-$secondary: $gray-900;
diff --git a/src/assets/css/themes/_variables.vaporwave-dark.scss b/src/assets/css/themes/_variables.vaporwave-dark.scss
deleted file mode 100644
index cbccc998..00000000
--- a/src/assets/css/themes/_variables.vaporwave-dark.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-$blue: #01cdfe;
-$indigo: #b967ff;
-$purple: #b967ff;
-$pink: rgb(255, 64, 186);
-$red: rgb(255, 95, 110);
-$orange: rgb(255, 167, 93);
-$yellow: #fffb96;
-$green: #05ffa1;
-$teal: #01cdfe;
-$cyan: #01cdfe;
-$enable-shadows: true;
-$enable-gradients: true;
-$enable-responsive-font-sizes: true;
-$body-bg: $gray-900;
-$body-color: $gray-200;
-$border-radius: 1rem;
-$border-radius-lg: 1rem;
-$font-family-monospace: Arial, "Noto Sans", sans-serif;
-$yiq-text-light: $gray-300;
-$secondary: $blue;
-$text-muted: $gray-500;
-$primary: $pink;
-$navbar-light-hover-color: rgba($primary, 0.7);
-$light: darken($gray-100, 1.5);
-$font-family-sans-serif: "Lucida Console", Monaco, monospace;
-$card-bg: $body-bg;
-$navbar-dark-color: rgba($body-bg, 0.5);
-$navbar-light-active-color: rgba($gray-200, 0.9);
-$navbar-light-disabled-color: rgba($gray-200, 0.3);
-$navbar-light-color: rgba($white, 0.5);
-$input-bg: $gray-700;
-$input-color: $gray-200;
-$input-disabled-bg: $gray-800;
-$input-border-color: $gray-800;
-$mark-bg: $gray-600;
-$pre-color: $gray-200;
-mark-bg: $gray-600;
diff --git a/src/assets/css/themes/_variables.vaporwave-light.scss b/src/assets/css/themes/_variables.vaporwave-light.scss
deleted file mode 100644
index 77495781..00000000
--- a/src/assets/css/themes/_variables.vaporwave-light.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-$blue: #01cdfe;
-$indigo: #b967ff;
-$purple: #b967ff;
-$pink: rgb(255, 64, 186);
-$red: rgb(255, 95, 110);
-$orange: rgb(255, 167, 93);
-$yellow: #fffb96;
-$green: #05ffa1;
-$teal: #01cdfe;
-$cyan: #01cdfe;
-$enable-shadows: true;
-$enable-gradients: true;
-$enable-responsive-font-sizes: true;
-$body-bg: $gray-100;
-$body-color: $gray-700;
-$border-radius: 1rem;
-$border-radius-lg: 1rem;
-$font-family-monospace: Arial, "Noto Sans", sans-serif;
-$yiq-text-light: $gray-300;
-$secondary: $blue;
-$text-muted: $gray-500;
-$primary: $pink;
-$navbar-light-hover-color: rgba($primary, 0.7);
-$light: darken($gray-100, 1.5);
-$font-family-sans-serif: "Lucida Console", Monaco, monospace;
diff --git a/src/client/index.tsx b/src/client/index.tsx
index 99f12371..7b6b6b1c 100644
--- a/src/client/index.tsx
+++ b/src/client/index.tsx
@@ -1,18 +1,19 @@
import { hydrate } from "inferno-hydrate";
-import { BrowserRouter } from "inferno-router";
+import { Router } from "inferno-router";
import { App } from "../shared/components/app/app";
import { initializeSite } from "../shared/utils";
import "bootstrap/js/dist/collapse";
import "bootstrap/js/dist/dropdown";
+import { HistoryService } from "../shared/services/HistoryService";
const site = window.isoData.site_res;
initializeSite(site);
const wrapper = (
-
+
-
+
);
const root = document.getElementById("root");
diff --git a/src/server/index.tsx b/src/server/index.tsx
index 4ab4f76f..98063558 100644
--- a/src/server/index.tsx
+++ b/src/server/index.tsx
@@ -6,19 +6,24 @@ import { Helmet } from "inferno-helmet";
import { matchPath, StaticRouter } from "inferno-router";
import { renderToString } from "inferno-server";
import IsomorphicCookie from "isomorphic-cookie";
-import { GetSite, GetSiteResponse, LemmyHttp, Site } from "lemmy-js-client";
+import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
import path from "path";
import process from "process";
import serialize from "serialize-javascript";
import sharp from "sharp";
import { App } from "../shared/components/app/app";
-import { getHttpBase, getHttpBaseInternal } from "../shared/env";
+import { getHttpBaseExternal, getHttpBaseInternal } from "../shared/env";
import {
ILemmyConfig,
InitialFetchRequest,
IsoDataOptionalSite,
} from "../shared/interfaces";
import { routes } from "../shared/routes";
+import {
+ FailedRequestState,
+ RequestState,
+ wrapClient,
+} from "../shared/services/HttpService";
import {
ErrorPageData,
favIconPngUrl,
@@ -38,7 +43,7 @@ if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
server.use(function (_req, res, next) {
res.setHeader(
"Content-Security-Policy",
- `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *`
+ `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *`
);
next();
});
@@ -64,7 +69,13 @@ Disallow: /search/
server.get("/service-worker.js", async (_req, res) => {
res.setHeader("Content-Type", "application/javascript");
- res.sendFile(path.resolve("./dist/service-worker.js"));
+ res.sendFile(
+ path.resolve(
+ `./dist/service-worker${
+ process.env.NODE_ENV === "development" ? "-development" : ""
+ }.js`
+ )
+ );
});
server.get("/robots.txt", async (_req, res) => {
@@ -121,7 +132,7 @@ server.get("/*", async (req, res) => {
const getSiteForm: GetSite = { auth };
const headers = setForwardedHeaders(req.headers);
- const client = new LemmyHttp(getHttpBaseInternal(), headers);
+ const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
const { path, url, query } = req;
@@ -129,27 +140,30 @@ server.get("/*", async (req, res) => {
// This bypasses errors, so that the client can hit the error on its own,
// in order to remove the jwt on the browser. Necessary for wrong jwts
let site: GetSiteResponse | undefined = undefined;
- let routeData: Record = {};
- let errorPageData: ErrorPageData | undefined;
- try {
- let try_site: any = await client.getSite(getSiteForm);
- if (try_site.error == "not_logged_in") {
- console.error(
- "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
- );
- getSiteForm.auth = undefined;
- auth = undefined;
- try_site = await client.getSite(getSiteForm);
- }
+ let routeData: Record> = {};
+ let errorPageData: ErrorPageData | undefined = undefined;
+ let try_site = await client.getSite(getSiteForm);
+ if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
+ console.error(
+ "Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
+ );
+ getSiteForm.auth = undefined;
+ auth = undefined;
+ try_site = await client.getSite(getSiteForm);
+ }
- if (!auth && isAuthPath(path)) {
- res.redirect("/login");
- return;
- }
+ if (!auth && isAuthPath(path)) {
+ return res.redirect("/login");
+ }
- site = try_site;
+ if (try_site.state === "success") {
+ site = try_site.data;
initializeSite(site);
+ if (path != "/setup" && !site.site_view.local_site.site_setup) {
+ return res.redirect("/setup");
+ }
+
if (site) {
const initialFetchReq: InitialFetchRequest = {
client,
@@ -173,19 +187,21 @@ server.get("/*", async (req, res) => {
}, {});
}
}
- } catch (error) {
- errorPageData = getErrorPageData(error, site);
+ } else if (try_site.state === "failed") {
+ errorPageData = getErrorPageData(new Error(try_site.msg), site);
}
- const error = Object.values(routeData).find(val => val?.error)?.error;
+ const error = Object.values(routeData).find(
+ res => res.state === "failed"
+ ) as FailedRequestState | undefined;
// Redirect to the 404 if there's an API error
if (error) {
- console.error(error);
- if (error === "instance_is_private") {
+ console.error(error.msg);
+ if (error.msg === "instance_is_private") {
return res.redirect(`/signup`);
} else {
- errorPageData = getErrorPageData(error, site);
+ errorPageData = getErrorPageData(new Error(error.msg), site);
}
}
@@ -222,15 +238,15 @@ server.listen(Number(port), hostname, () => {
function setForwardedHeaders(headers: IncomingHttpHeaders): {
[key: string]: string;
} {
- let out: { [key: string]: string } = {};
+ const out: { [key: string]: string } = {};
if (headers.host) {
out.host = headers.host;
}
- let realIp = headers["x-real-ip"];
+ const realIp = headers["x-real-ip"];
if (realIp) {
out["x-real-ip"] = realIp as string;
}
- let forwardedFor = headers["x-forwarded-for"];
+ const forwardedFor = headers["x-forwarded-for"];
if (forwardedFor) {
out["x-forwarded-for"] = forwardedFor as string;
}
@@ -243,7 +259,7 @@ process.on("SIGINT", () => {
process.exit(0);
});
-const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
+const iconSizes = [72, 96, 144, 192, 512];
const defaultLogoPathDirectory = path.join(
process.cwd(),
"dist",
@@ -251,12 +267,15 @@ const defaultLogoPathDirectory = path.join(
"icons"
);
-export async function generateManifestBase64(site: Site) {
- const url = (
- process.env.NODE_ENV === "development"
- ? "http://localhost:1236/"
- : getHttpBase()
- ).replace(/\/$/g, "");
+export async function generateManifestBase64({
+ my_user,
+ site_view: {
+ site,
+ local_site: { community_creation_admin_only },
+ },
+}: GetSiteResponse) {
+ const url = getHttpBaseExternal();
+
const icon = site.icon ? await fetchIconPng(site.icon) : null;
const manifest = {
@@ -290,15 +309,58 @@ export async function generateManifestBase64(site: Site) {
};
})
),
+ shortcuts: [
+ {
+ name: "Search",
+ short_name: "Search",
+ description: "Perform a search.",
+ url: "/search",
+ },
+ {
+ name: "Communities",
+ url: "/communities",
+ short_name: "Communities",
+ description: "Browse communities",
+ },
+ ]
+ .concat(
+ my_user
+ ? [
+ {
+ name: "Create Post",
+ url: "/create_post",
+ short_name: "Create Post",
+ description: "Create a post.",
+ },
+ ]
+ : []
+ )
+ .concat(
+ my_user?.local_user_view.person.admin || !community_creation_admin_only
+ ? [
+ {
+ name: "Create Community",
+ url: "/create_community",
+ short_name: "Create Community",
+ description: "Create a community",
+ },
+ ]
+ : []
+ ),
+ related_applications: [
+ {
+ platform: "f-droid",
+ url: "https://f-droid.org/packages/com.jerboa/",
+ id: "com.jerboa",
+ },
+ ],
};
return Buffer.from(JSON.stringify(manifest)).toString("base64");
}
async function fetchIconPng(iconUrl: string) {
- return await fetch(
- iconUrl.replace(/https?:\/\/[^\/]+/g, getHttpBaseInternal())
- )
+ return await fetch(iconUrl)
.then(res => res.blob())
.then(blob => blob.arrayBuffer());
}
@@ -339,14 +401,15 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
.then(buf => buf.toString("base64"))}`
: favIconPngUrl;
- const eruda = (
- <>
-
-
- >
- );
-
- const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : "";
+ const erudaStr =
+ process.env["LEMMY_UI_DEBUG"] === "true"
+ ? renderToString(
+ <>
+
+
+ >
+ )
+ : "";
const helmet = Helmet.renderStatic();
@@ -354,9 +417,9 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
return `
-
+
-
+
@@ -384,9 +447,9 @@ async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
site &&
` `
}
diff --git a/src/shared/components/app/app.tsx b/src/shared/components/app/app.tsx
index 9e6e9bdf..96857f31 100644
--- a/src/shared/components/app/app.tsx
+++ b/src/shared/components/app/app.tsx
@@ -1,8 +1,8 @@
import { Component } from "inferno";
import { Provider } from "inferno-i18next-dess";
import { Route, Switch } from "inferno-router";
-import { IsoDataOptionalSite } from "shared/interfaces";
import { i18n } from "../../i18next";
+import { IsoDataOptionalSite } from "../../interfaces";
import { routes } from "../../routes";
import { isAuthPath, setIsoData } from "../../utils";
import AuthGuard from "../common/auth-guard";
diff --git a/src/shared/components/app/navbar.tsx b/src/shared/components/app/navbar.tsx
index 25c3a306..bdbac9ff 100644
--- a/src/shared/components/app/navbar.tsx
+++ b/src/shared/components/app/navbar.tsx
@@ -1,35 +1,23 @@
import { Component, createRef, linkEvent } from "inferno";
import { NavLink } from "inferno-router";
import {
- CommentResponse,
- GetReportCount,
GetReportCountResponse,
GetSiteResponse,
- GetUnreadCount,
GetUnreadCountResponse,
- GetUnreadRegistrationApplicationCount,
GetUnreadRegistrationApplicationCountResponse,
- PrivateMessageResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
-import { UserService, WebSocketService } from "../../services";
+import { UserService } from "../../services";
+import { HttpService, RequestState } from "../../services/HttpService";
import {
amAdmin,
canCreateCommunity,
donateLemmyUrl,
isBrowser,
myAuth,
- notifyComment,
- notifyPrivateMessage,
numToSI,
showAvatars,
toast,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { Icon } from "../common/icon";
import { PictrsImage } from "../common/pictrs-image";
@@ -39,9 +27,9 @@ interface NavbarProps {
}
interface NavbarState {
- unreadInboxCount: number;
- unreadReportCount: number;
- unreadApplicationCount: number;
+ unreadInboxCountRes: RequestState;
+ unreadReportCountRes: RequestState;
+ unreadApplicationCountRes: RequestState;
onSiteBanner?(url: string): any;
}
@@ -51,77 +39,48 @@ function handleCollapseClick(i: Navbar) {
}
}
-function handleLogOut() {
+function handleLogOut(i: Navbar) {
UserService.Instance.logout();
+ handleCollapseClick(i);
}
export class Navbar extends Component {
- private wsSub: Subscription;
- private userSub: Subscription;
- private unreadInboxCountSub: Subscription;
- private unreadReportCountSub: Subscription;
- private unreadApplicationCountSub: Subscription;
state: NavbarState = {
- unreadInboxCount: 0,
- unreadReportCount: 0,
- unreadApplicationCount: 0,
+ unreadInboxCountRes: { state: "empty" },
+ unreadReportCountRes: { state: "empty" },
+ unreadApplicationCountRes: { state: "empty" },
};
- subscription: any;
collapseButtonRef = createRef();
+ mobileMenuRef = createRef();
constructor(props: any, context: any) {
super(props, context);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleOutsideMenuClick = this.handleOutsideMenuClick.bind(this);
}
- componentDidMount() {
+ async componentDidMount() {
// Subscribe to jwt changes
if (isBrowser()) {
// On the first load, check the unreads
- let auth = myAuth(false);
- if (auth && UserService.Instance.myUserInfo) {
- this.requestNotificationPermission();
- WebSocketService.Instance.send(
- wsClient.userJoin({
- auth,
- })
- );
-
- this.fetchUnreads();
- }
-
+ this.requestNotificationPermission();
+ await this.fetchUnreads();
this.requestNotificationPermission();
- // Subscribe to unread count changes
- this.unreadInboxCountSub =
- UserService.Instance.unreadInboxCountSub.subscribe(res => {
- this.setState({ unreadInboxCount: res });
- });
- // Subscribe to unread report count changes
- this.unreadReportCountSub =
- UserService.Instance.unreadReportCountSub.subscribe(res => {
- this.setState({ unreadReportCount: res });
- });
- // Subscribe to unread application count
- this.unreadApplicationCountSub =
- UserService.Instance.unreadApplicationCountSub.subscribe(res => {
- this.setState({ unreadApplicationCount: res });
- });
+ document.addEventListener("mouseup", this.handleOutsideMenuClick);
}
}
componentWillUnmount() {
- this.wsSub.unsubscribe();
- this.userSub.unsubscribe();
- this.unreadInboxCountSub.unsubscribe();
- this.unreadReportCountSub.unsubscribe();
- this.unreadApplicationCountSub.unsubscribe();
+ document.removeEventListener("mouseup", this.handleOutsideMenuClick);
+ }
+
+ render() {
+ return this.navbar();
}
// TODO class active corresponding to current page
- render() {
+ navbar() {
const siteView = this.props.siteRes?.site_view;
const person = UserService.Instance.myUserInfo?.local_user_view.person;
return (
@@ -144,15 +103,15 @@ export class Navbar extends Component {
to="/inbox"
className="p-1 nav-link border-0"
title={i18n.t("unread_messages", {
- count: Number(this.state.unreadInboxCount),
- formattedCount: numToSI(this.state.unreadInboxCount),
+ count: Number(this.state.unreadApplicationCountRes.state),
+ formattedCount: numToSI(this.unreadInboxCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
- {this.state.unreadInboxCount > 0 && (
+ {this.unreadInboxCount > 0 && (
- {numToSI(this.state.unreadInboxCount)}
+ {numToSI(this.unreadInboxCount)}
)}
@@ -163,15 +122,15 @@ export class Navbar extends Component {
to="/reports"
className="p-1 nav-link border-0"
title={i18n.t("unread_reports", {
- count: Number(this.state.unreadReportCount),
- formattedCount: numToSI(this.state.unreadReportCount),
+ count: Number(this.unreadReportCount),
+ formattedCount: numToSI(this.unreadReportCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
- {this.state.unreadReportCount > 0 && (
+ {this.unreadReportCount > 0 && (
- {numToSI(this.state.unreadReportCount)}
+ {numToSI(this.unreadReportCount)}
)}
@@ -183,15 +142,15 @@ export class Navbar extends Component {
to="/registration_applications"
className="p-1 nav-link border-0"
title={i18n.t("unread_registration_applications", {
- count: Number(this.state.unreadApplicationCount),
- formattedCount: numToSI(this.state.unreadApplicationCount),
+ count: Number(this.unreadApplicationCount),
+ formattedCount: numToSI(this.unreadApplicationCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
- {this.state.unreadApplicationCount > 0 && (
+ {this.unreadApplicationCount > 0 && (
- {numToSI(this.state.unreadApplicationCount)}
+ {numToSI(this.unreadApplicationCount)}
)}
@@ -212,7 +171,11 @@ export class Navbar extends Component {
>
-
+
- {!this.context.router.history.location.pathname.match(
- /^\/search/
- ) && (
-
-
-
-
-
- )}
+
+
+
+
+
{amAdmin() && (
{
className="nav-link"
to="/inbox"
title={i18n.t("unread_messages", {
- count: Number(this.state.unreadInboxCount),
- formattedCount: numToSI(this.state.unreadInboxCount),
+ count: Number(this.unreadInboxCount),
+ formattedCount: numToSI(this.unreadInboxCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
- {this.state.unreadInboxCount > 0 && (
-
- {numToSI(this.state.unreadInboxCount)}
+ {this.unreadInboxCount > 0 && (
+
+ {numToSI(this.unreadInboxCount)}
)}
@@ -316,15 +275,15 @@ export class Navbar extends Component {
className="nav-link"
to="/reports"
title={i18n.t("unread_reports", {
- count: Number(this.state.unreadReportCount),
- formattedCount: numToSI(this.state.unreadReportCount),
+ count: Number(this.unreadReportCount),
+ formattedCount: numToSI(this.unreadReportCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
- {this.state.unreadReportCount > 0 && (
-
- {numToSI(this.state.unreadReportCount)}
+ {this.unreadReportCount > 0 && (
+
+ {numToSI(this.unreadReportCount)}
)}
@@ -336,17 +295,15 @@ export class Navbar extends Component {
to="/registration_applications"
className="nav-link"
title={i18n.t("unread_registration_applications", {
- count: Number(this.state.unreadApplicationCount),
- formattedCount: numToSI(
- this.state.unreadApplicationCount
- ),
+ count: Number(this.unreadApplicationCount),
+ formattedCount: numToSI(this.unreadApplicationCount),
})}
onMouseUp={linkEvent(this, handleCollapseClick)}
>
- {this.state.unreadApplicationCount > 0 && (
+ {this.unreadApplicationCount > 0 && (
- {numToSI(this.state.unreadApplicationCount)}
+ {numToSI(this.unreadApplicationCount)}
)}
@@ -397,7 +354,7 @@ export class Navbar extends Component {
{i18n.t("logout")}
@@ -437,107 +394,77 @@ export class Navbar extends Component {
);
}
+ handleOutsideMenuClick(event: MouseEvent) {
+ if (!this.mobileMenuRef.current?.contains(event.target as Node | null)) {
+ handleCollapseClick(this);
+ }
+ }
+
get moderatesSomething(): boolean {
- let mods = UserService.Instance.myUserInfo?.moderates;
- let moderatesS = (mods && mods.length > 0) || false;
+ const mods = UserService.Instance.myUserInfo?.moderates;
+ const moderatesS = (mods && mods.length > 0) || false;
return amAdmin() || moderatesS;
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- if (msg.error == "not_logged_in") {
- UserService.Instance.logout();
- }
- return;
- } else if (msg.reconnect) {
- console.log(i18n.t("websocket_reconnected"));
- let auth = myAuth(false);
- if (UserService.Instance.myUserInfo && auth) {
- WebSocketService.Instance.send(
- wsClient.userJoin({
- auth,
- })
- );
- this.fetchUnreads();
- }
- } else if (op == UserOperation.GetUnreadCount) {
- let data = wsJsonToRes(msg);
+ async fetchUnreads() {
+ const auth = myAuth();
+ if (auth) {
+ this.setState({ unreadInboxCountRes: { state: "loading" } });
this.setState({
- unreadInboxCount: data.replies + data.mentions + data.private_messages,
+ unreadInboxCountRes: await HttpService.client.getUnreadCount({
+ auth,
+ }),
});
- this.sendUnreadCount();
- } else if (op == UserOperation.GetReportCount) {
- let data = wsJsonToRes(msg);
- this.setState({
- unreadReportCount:
- data.post_reports +
- data.comment_reports +
- (data.private_message_reports ?? 0),
- });
- this.sendReportUnread();
- } else if (op == UserOperation.GetUnreadRegistrationApplicationCount) {
- let data =
- wsJsonToRes(msg);
- this.setState({ unreadApplicationCount: data.registration_applications });
- this.sendApplicationUnread();
- } else if (op == UserOperation.CreateComment) {
- let data = wsJsonToRes(msg);
- let mui = UserService.Instance.myUserInfo;
- if (
- mui &&
- data.recipient_ids.includes(mui.local_user_view.local_user.id)
- ) {
- this.setState({
- unreadInboxCount: this.state.unreadInboxCount + 1,
- });
- this.sendUnreadCount();
- notifyComment(data.comment_view, this.context.router);
- }
- } else if (op == UserOperation.CreatePrivateMessage) {
- let data = wsJsonToRes(msg);
- if (
- data.private_message_view.recipient.id ==
- UserService.Instance.myUserInfo?.local_user_view.person.id
- ) {
+ if (this.moderatesSomething) {
+ this.setState({ unreadReportCountRes: { state: "loading" } });
this.setState({
- unreadInboxCount: this.state.unreadInboxCount + 1,
+ unreadReportCountRes: await HttpService.client.getReportCount({
+ auth,
+ }),
+ });
+ }
+
+ if (amAdmin()) {
+ this.setState({ unreadApplicationCountRes: { state: "loading" } });
+ this.setState({
+ unreadApplicationCountRes:
+ await HttpService.client.getUnreadRegistrationApplicationCount({
+ auth,
+ }),
});
- this.sendUnreadCount();
- notifyPrivateMessage(data.private_message_view, this.context.router);
}
}
}
- fetchUnreads() {
- console.log("Fetching inbox unreads...");
+ get unreadInboxCount(): number {
+ if (this.state.unreadInboxCountRes.state == "success") {
+ const data = this.state.unreadInboxCountRes.data;
+ return data.replies + data.mentions + data.private_messages;
+ } else {
+ return 0;
+ }
+ }
- let auth = myAuth();
- if (auth) {
- let unreadForm: GetUnreadCount = {
- auth,
- };
- WebSocketService.Instance.send(wsClient.getUnreadCount(unreadForm));
+ get unreadReportCount(): number {
+ if (this.state.unreadReportCountRes.state == "success") {
+ const data = this.state.unreadReportCountRes.data;
+ return (
+ data.post_reports +
+ data.comment_reports +
+ (data.private_message_reports ?? 0)
+ );
+ } else {
+ return 0;
+ }
+ }
- console.log("Fetching reports...");
-
- let reportCountForm: GetReportCount = {
- auth,
- };
- WebSocketService.Instance.send(wsClient.getReportCount(reportCountForm));
-
- if (amAdmin()) {
- console.log("Fetching applications...");
-
- let applicationCountForm: GetUnreadRegistrationApplicationCount = {
- auth,
- };
- WebSocketService.Instance.send(
- wsClient.getUnreadRegistrationApplicationCount(applicationCountForm)
- );
- }
+ get unreadApplicationCount(): number {
+ if (this.state.unreadApplicationCountRes.state == "success") {
+ const data = this.state.unreadApplicationCountRes.data;
+ return data.registration_applications;
+ } else {
+ return 0;
}
}
@@ -545,22 +472,6 @@ export class Navbar extends Component {
return this.context.router.history.location.pathname;
}
- sendUnreadCount() {
- UserService.Instance.unreadInboxCountSub.next(this.state.unreadInboxCount);
- }
-
- sendReportUnread() {
- UserService.Instance.unreadReportCountSub.next(
- this.state.unreadReportCount
- );
- }
-
- sendApplicationUnread() {
- UserService.Instance.unreadApplicationCountSub.next(
- this.state.unreadApplicationCount
- );
- }
-
requestNotificationPermission() {
if (UserService.Instance.myUserInfo) {
document.addEventListener("DOMContentLoaded", function () {
diff --git a/src/shared/components/app/theme.tsx b/src/shared/components/app/theme.tsx
index 4510fe2c..941eea2c 100644
--- a/src/shared/components/app/theme.tsx
+++ b/src/shared/components/app/theme.tsx
@@ -8,8 +8,8 @@ interface Props {
export class Theme extends Component {
render() {
- let user = UserService.Instance.myUserInfo;
- let hasTheme = user?.local_user_view.local_user.theme !== "browser";
+ const user = UserService.Instance.myUserInfo;
+ const hasTheme = user?.local_user_view.local_user.theme !== "browser";
if (user && hasTheme) {
return (
diff --git a/src/shared/components/comment/comment-form.tsx b/src/shared/components/comment/comment-form.tsx
index 74d17bff..c60cde20 100644
--- a/src/shared/components/comment/comment-form.tsx
+++ b/src/shared/components/comment/comment-form.tsx
@@ -1,25 +1,11 @@
import { Component } from "inferno";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
-import {
- CommentResponse,
- CreateComment,
- EditComment,
- Language,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
-} from "lemmy-js-client";
-import { Subscription } from "rxjs";
-import { CommentNodeI } from "shared/interfaces";
+import { CreateComment, EditComment, Language } from "lemmy-js-client";
import { i18n } from "../../i18next";
-import { UserService, WebSocketService } from "../../services";
-import {
- capitalizeFirstLetter,
- myAuth,
- wsClient,
- wsSubscribe,
-} from "../../utils";
+import { CommentNodeI } from "../../interfaces";
+import { UserService } from "../../services";
+import { capitalizeFirstLetter, myAuthRequired } from "../../utils";
import { Icon } from "../common/icon";
import { MarkdownTextArea } from "../common/markdown-textarea";
@@ -28,48 +14,25 @@ interface CommentFormProps {
* Can either be the parent, or the editable comment. The right side is a postId.
*/
node: CommentNodeI | number;
+ finished?: boolean;
edit?: boolean;
disabled?: boolean;
focus?: boolean;
- onReplyCancel?(): any;
+ onReplyCancel?(): void;
allLanguages: Language[];
siteLanguages: number[];
+ onUpsertComment(form: EditComment | CreateComment): void;
}
-interface CommentFormState {
- buttonTitle: string;
- finished: boolean;
- formId?: string;
-}
-
-export class CommentForm extends Component {
- private subscription?: Subscription;
- state: CommentFormState = {
- buttonTitle:
- typeof this.props.node === "number"
- ? capitalizeFirstLetter(i18n.t("post"))
- : this.props.edit
- ? capitalizeFirstLetter(i18n.t("save"))
- : capitalizeFirstLetter(i18n.t("reply")),
- finished: false,
- };
-
+export class CommentForm extends Component {
constructor(props: any, context: any) {
super(props, context);
this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
- this.handleReplyCancel = this.handleReplyCancel.bind(this);
-
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
- }
-
- componentWillUnmount() {
- this.subscription?.unsubscribe();
}
render() {
- let initialContent =
+ const initialContent =
typeof this.props.node !== "number"
? this.props.edit
? this.props.node.comment_view.comment.content
@@ -82,13 +45,13 @@ export class CommentForm extends Component {
{
);
}
- handleCommentSubmit(msg: {
- val: string;
- formId: string;
- languageId?: number;
- }) {
- let content = msg.val;
- let language_id = msg.languageId;
- let node = this.props.node;
+ get buttonTitle(): string {
+ return typeof this.props.node === "number"
+ ? capitalizeFirstLetter(i18n.t("post"))
+ : this.props.edit
+ ? capitalizeFirstLetter(i18n.t("save"))
+ : capitalizeFirstLetter(i18n.t("reply"));
+ }
- this.setState({ formId: msg.formId });
-
- let auth = myAuth();
- if (auth) {
- if (typeof node === "number") {
- let postId = node;
- let form: CreateComment = {
+ handleCommentSubmit(content: string, form_id: string, language_id?: number) {
+ const { node, onUpsertComment, edit } = this.props;
+ if (typeof node === "number") {
+ const post_id = node;
+ onUpsertComment({
+ content,
+ post_id,
+ language_id,
+ form_id,
+ auth: myAuthRequired(),
+ });
+ } else {
+ if (edit) {
+ const comment_id = node.comment_view.comment.id;
+ onUpsertComment({
content,
- form_id: this.state.formId,
- post_id: postId,
+ comment_id,
+ form_id,
language_id,
- auth,
- };
- WebSocketService.Instance.send(wsClient.createComment(form));
+ auth: myAuthRequired(),
+ });
} else {
- if (this.props.edit) {
- let form: EditComment = {
- content,
- form_id: this.state.formId,
- comment_id: node.comment_view.comment.id,
- language_id,
- auth,
- };
- WebSocketService.Instance.send(wsClient.editComment(form));
- } else {
- let form: CreateComment = {
- content,
- form_id: this.state.formId,
- post_id: node.comment_view.post.id,
- parent_id: node.comment_view.comment.id,
- language_id,
- auth,
- };
- WebSocketService.Instance.send(wsClient.createComment(form));
- }
- }
- }
- }
-
- handleReplyCancel() {
- this.props.onReplyCancel?.();
- }
-
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
-
- // Only do the showing and hiding if logged in
- if (UserService.Instance.myUserInfo) {
- if (
- op == UserOperation.CreateComment ||
- op == UserOperation.EditComment
- ) {
- let data = wsJsonToRes(msg);
-
- // This only finishes this form, if the randomly generated form_id matches the one received
- if (this.state.formId && this.state.formId == data.form_id) {
- this.setState({ finished: true });
-
- // Necessary because it broke tribute for some reason
- this.setState({ finished: false });
- }
+ const post_id = node.comment_view.post.id;
+ const parent_id = node.comment_view.comment.id;
+ this.props.onUpsertComment({
+ content,
+ parent_id,
+ post_id,
+ form_id,
+ language_id,
+ auth: myAuthRequired(),
+ });
}
}
}
diff --git a/src/shared/components/comment/comment-node.tsx b/src/shared/components/comment/comment-node.tsx
index 7ed5e8f4..8559f38b 100644
--- a/src/shared/components/comment/comment-node.tsx
+++ b/src/shared/components/comment/comment-node.tsx
@@ -1,5 +1,5 @@
import classNames from "classnames";
-import { Component, linkEvent } from "inferno";
+import { Component, InfernoNode, linkEvent } from "inferno";
import { Link } from "inferno-router";
import {
AddAdmin,
@@ -7,13 +7,16 @@ import {
BanFromCommunity,
BanPerson,
BlockPerson,
+ CommentId,
CommentReplyView,
CommentView,
CommunityModeratorView,
+ CreateComment,
CreateCommentLike,
CreateCommentReport,
DeleteComment,
DistinguishComment,
+ EditComment,
GetComments,
Language,
MarkCommentReplyAsRead,
@@ -33,8 +36,9 @@ import {
CommentNodeI,
CommentViewType,
PurgeType,
+ VoteType,
} from "../../interfaces";
-import { UserService, WebSocketService } from "../../services";
+import { UserService } from "../../services";
import {
amCommunityCreator,
canAdmin,
@@ -49,10 +53,11 @@ import {
mdToHtml,
mdToHtmlNoImages,
myAuth,
+ myAuthRequired,
+ newVote,
numToSI,
setupTippy,
showScores,
- wsClient,
} from "../../utils";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
import { MomentTime } from "../common/moment-time";
@@ -74,7 +79,6 @@ interface CommentNodeState {
showPurgeDialog: boolean;
purgeReason?: string;
purgeType: PurgeType;
- purgeLoading: boolean;
showConfirmTransferSite: boolean;
showConfirmTransferCommunity: boolean;
showConfirmAppointAsMod: boolean;
@@ -84,12 +88,22 @@ interface CommentNodeState {
showAdvanced: boolean;
showReportDialog: boolean;
reportReason?: string;
- my_vote?: number;
- score: number;
- upvotes: number;
- downvotes: number;
- readLoading: boolean;
+ createOrEditCommentLoading: boolean;
+ upvoteLoading: boolean;
+ downvoteLoading: boolean;
saveLoading: boolean;
+ readLoading: boolean;
+ blockPersonLoading: boolean;
+ deleteLoading: boolean;
+ removeLoading: boolean;
+ distinguishLoading: boolean;
+ banLoading: boolean;
+ addModLoading: boolean;
+ addAdminLoading: boolean;
+ transferCommunityLoading: boolean;
+ fetchChildrenLoading: boolean;
+ reportLoading: boolean;
+ purgeLoading: boolean;
}
interface CommentNodeProps {
@@ -108,6 +122,26 @@ interface CommentNodeProps {
allLanguages: Language[];
siteLanguages: number[];
hideImages?: boolean;
+ finished: Map;
+ onSaveComment(form: SaveComment): void;
+ onCommentReplyRead(form: MarkCommentReplyAsRead): void;
+ onPersonMentionRead(form: MarkPersonMentionAsRead): void;
+ onCreateComment(form: EditComment | CreateComment): void;
+ onEditComment(form: EditComment | CreateComment): void;
+ onCommentVote(form: CreateCommentLike): void;
+ onBlockPerson(form: BlockPerson): void;
+ onDeleteComment(form: DeleteComment): void;
+ onRemoveComment(form: RemoveComment): void;
+ onDistinguishComment(form: DistinguishComment): void;
+ onAddModToCommunity(form: AddModToCommunity): void;
+ onAddAdmin(form: AddAdmin): void;
+ onBanPersonFromCommunity(form: BanFromCommunity): void;
+ onBanPerson(form: BanPerson): void;
+ onTransferCommunity(form: TransferCommunity): void;
+ onFetchChildren?(form: GetComments): void;
+ onCommentReport(form: CreateCommentReport): void;
+ onPurgePerson(form: PurgePerson): void;
+ onPurgeComment(form: PurgeComment): void;
}
export class CommentNode extends Component {
@@ -119,7 +153,6 @@ export class CommentNode extends Component {
removeData: false,
banType: BanType.Community,
showPurgeDialog: false,
- purgeLoading: false,
purgeType: PurgeType.Person,
collapsed: false,
viewSource: false,
@@ -129,80 +162,122 @@ export class CommentNode extends Component {
showConfirmAppointAsMod: false,
showConfirmAppointAsAdmin: false,
showReportDialog: false,
- my_vote: this.props.node.comment_view.my_vote,
- score: this.props.node.comment_view.counts.score,
- upvotes: this.props.node.comment_view.counts.upvotes,
- downvotes: this.props.node.comment_view.counts.downvotes,
- readLoading: false,
+ createOrEditCommentLoading: false,
+ upvoteLoading: false,
+ downvoteLoading: false,
saveLoading: false,
+ readLoading: false,
+ blockPersonLoading: false,
+ deleteLoading: false,
+ removeLoading: false,
+ distinguishLoading: false,
+ banLoading: false,
+ addModLoading: false,
+ addAdminLoading: false,
+ transferCommunityLoading: false,
+ fetchChildrenLoading: false,
+ reportLoading: false,
+ purgeLoading: false,
};
constructor(props: any, context: any) {
super(props, context);
this.handleReplyCancel = this.handleReplyCancel.bind(this);
- this.handleCommentUpvote = this.handleCommentUpvote.bind(this);
- this.handleCommentDownvote = this.handleCommentDownvote.bind(this);
}
- // TODO see if there's a better way to do this, and all willReceiveProps
- componentWillReceiveProps(nextProps: CommentNodeProps) {
- let cv = nextProps.node.comment_view;
- this.setState({
- my_vote: cv.my_vote,
- upvotes: cv.counts.upvotes,
- downvotes: cv.counts.downvotes,
- score: cv.counts.score,
- readLoading: false,
- saveLoading: false,
- });
+ get commentView(): CommentView {
+ return this.props.node.comment_view;
+ }
+
+ get commentId(): CommentId {
+ return this.commentView.comment.id;
+ }
+
+ componentWillReceiveProps(
+ nextProps: Readonly<{ children?: InfernoNode } & CommentNodeProps>
+ ): void {
+ if (this.props != nextProps) {
+ this.setState({
+ showReply: false,
+ showEdit: false,
+ showRemoveDialog: false,
+ showBanDialog: false,
+ removeData: false,
+ banType: BanType.Community,
+ showPurgeDialog: false,
+ purgeType: PurgeType.Person,
+ collapsed: false,
+ viewSource: false,
+ showAdvanced: false,
+ showConfirmTransferSite: false,
+ showConfirmTransferCommunity: false,
+ showConfirmAppointAsMod: false,
+ showConfirmAppointAsAdmin: false,
+ showReportDialog: false,
+ createOrEditCommentLoading: false,
+ upvoteLoading: false,
+ downvoteLoading: false,
+ saveLoading: false,
+ readLoading: false,
+ blockPersonLoading: false,
+ deleteLoading: false,
+ removeLoading: false,
+ distinguishLoading: false,
+ banLoading: false,
+ addModLoading: false,
+ addAdminLoading: false,
+ transferCommunityLoading: false,
+ fetchChildrenLoading: false,
+ reportLoading: false,
+ purgeLoading: false,
+ });
+ }
}
render() {
- let node = this.props.node;
- let cv = this.props.node.comment_view;
+ const node = this.props.node;
+ const cv = this.commentView;
- let purgeTypeText =
+ const purgeTypeText =
this.state.purgeType == PurgeType.Comment
? i18n.t("purge_comment")
: `${i18n.t("purge")} ${cv.creator.name}`;
- let canMod_ =
- canMod(cv.creator.id, this.props.moderators, this.props.admins) &&
- cv.community.local;
- let canModOnSelf =
- canMod(
- cv.creator.id,
- this.props.moderators,
- this.props.admins,
- UserService.Instance.myUserInfo,
- true
- ) && cv.community.local;
- let canAdmin_ =
- canAdmin(cv.creator.id, this.props.admins) && cv.community.local;
- let canAdminOnSelf =
- canAdmin(
- cv.creator.id,
- this.props.admins,
- UserService.Instance.myUserInfo,
- true
- ) && cv.community.local;
- let isMod_ = isMod(cv.creator.id, this.props.moderators);
- let isAdmin_ =
- isAdmin(cv.creator.id, this.props.admins) && cv.community.local;
- let amCommunityCreator_ = amCommunityCreator(
+ const canMod_ = canMod(
+ cv.creator.id,
+ this.props.moderators,
+ this.props.admins
+ );
+ const canModOnSelf = canMod(
+ cv.creator.id,
+ this.props.moderators,
+ this.props.admins,
+ UserService.Instance.myUserInfo,
+ true
+ );
+ const canAdmin_ = canAdmin(cv.creator.id, this.props.admins);
+ const canAdminOnSelf = canAdmin(
+ cv.creator.id,
+ this.props.admins,
+ UserService.Instance.myUserInfo,
+ true
+ );
+ const isMod_ = isMod(cv.creator.id, this.props.moderators);
+ const isAdmin_ = isAdmin(cv.creator.id, this.props.admins);
+ const amCommunityCreator_ = amCommunityCreator(
cv.creator.id,
this.props.moderators
);
- let borderColor = this.props.node.depth
+ const borderColor = this.props.node.depth
? colorList[(this.props.node.depth - 1) % colorList.length]
: colorList[0];
- let moreRepliesBorderColor = this.props.node.depth
+ const moreRepliesBorderColor = this.props.node.depth
? colorList[this.props.node.depth % colorList.length]
: colorList[0];
- let showMoreChildren =
+ const showMoreChildren =
this.props.viewType == CommentViewType.Tree &&
!this.state.collapsed &&
node.children.length == 0 &&
@@ -218,9 +293,7 @@ export class CommentNode extends Component {
id={`comment-${cv.comment.id}`}
className={classNames(`details comment-node py-2`, {
"border-top border-light": !this.props.noBorder,
- mark:
- this.isCommentNew ||
- this.props.node.comment_view.comment.distinguished,
+ mark: this.isCommentNew || this.commentView.comment.distinguished,
})}
style={
!this.props.noIndent && this.props.node.depth
@@ -234,6 +307,17 @@ export class CommentNode extends Component {
})}
>
+
+
+
@@ -270,18 +354,6 @@ export class CommentNode extends Component
{
>
)}
-
- {this.state.collapsed ? (
-
- ) : (
-
- )}
-
{this.linkBtn(true)}
{cv.comment.language_id !== 0 && (
@@ -298,18 +370,24 @@ export class CommentNode extends Component {
<>
-
- {numToSI(this.state.score)}
-
+ {this.state.upvoteLoading ? (
+
+ ) : (
+
+ {numToSI(this.commentView.counts.score)}
+
+ )}
•
>
@@ -328,9 +406,13 @@ export class CommentNode extends Component {
edit
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
+ finished={this.props.finished.get(
+ this.props.node.comment_view.comment.id
+ )}
focus
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
+ onUpsertComment={this.props.onEditComment}
/>
)}
{!this.state.showEdit && !this.state.collapsed && (
@@ -352,7 +434,7 @@ export class CommentNode extends Component {
{this.props.markable && (
{
}
>
{this.state.readLoading ? (
- this.loadingIcon
+
) : (
{
<>
-
- {showScores() &&
- this.state.upvotes !== this.state.score && (
-
- {numToSI(this.state.upvotes)}
-
- )}
+ {this.state.upvoteLoading ? (
+
+ ) : (
+ <>
+
+ {showScores() &&
+ this.commentView.counts.upvotes !==
+ this.commentView.counts.score && (
+
+ {numToSI(this.commentView.counts.upvotes)}
+
+ )}
+ >
+ )}
{this.props.enableDownvotes && (
-
- {showScores() &&
- this.state.upvotes !== this.state.score && (
-
- {numToSI(this.state.downvotes)}
-
- )}
+ {this.state.downvoteLoading ? (
+
+ ) : (
+ <>
+
+ {showScores() &&
+ this.commentView.counts.upvotes !==
+ this.commentView.counts.score && (
+
+ {numToSI(this.commentView.counts.downvotes)}
+
+ )}
+ >
+ )}
)}
{
<>
{!this.myComment && (
<>
-
-
-
-
-
+
+
+
{
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
- this.handleBlockUserClick
+ this.handleBlockPerson
)}
data-tippy-content={i18n.t("block_user")}
aria-label={i18n.t("block_user")}
>
-
+ {this.state.blockPersonLoading ? (
+
+ ) : (
+
+ )}
>
)}
{
}
>
{this.state.saveLoading ? (
- this.loadingIcon
+
) : (
{
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
- this.handleDeleteClick
+ this.handleDeleteComment
)}
data-tippy-content={
!cv.comment.deleted
@@ -534,12 +633,16 @@ export class CommentNode extends Component {
: i18n.t("restore")
}
>
-
+ {this.state.deleteLoading ? (
+
+ ) : (
+
+ )}
{(canModOnSelf || canAdminOnSelf) && (
@@ -547,7 +650,7 @@ export class CommentNode extends Component {
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
- this.handleDistinguishClick
+ this.handleDistinguishComment
)}
data-tippy-content={
!cv.comment.distinguished
@@ -589,11 +692,15 @@ export class CommentNode extends Component {
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
- this.handleModRemoveSubmit
+ this.handleRemoveComment
)}
aria-label={i18n.t("restore")}
>
- {i18n.t("restore")}
+ {this.state.removeLoading ? (
+
+ ) : (
+ i18n.t("restore")
+ )}
)}
>
@@ -618,11 +725,15 @@ export class CommentNode extends Component {
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
- this.handleModBanFromCommunitySubmit
+ this.handleBanPersonFromCommunity
)}
aria-label={i18n.t("unban")}
>
- {i18n.t("unban")}
+ {this.state.banLoading ? (
+
+ ) : (
+ i18n.t("unban")
+ )}
))}
{!cv.creator_banned_from_community &&
@@ -659,7 +770,11 @@ export class CommentNode extends Component {
)}
aria-label={i18n.t("yes")}
>
- {i18n.t("yes")}
+ {this.state.addModLoading ? (
+
+ ) : (
+ i18n.t("yes")
+ )}
{
)}
aria-label={i18n.t("yes")}
>
- {i18n.t("yes")}
+ {this.state.transferCommunityLoading ? (
+
+ ) : (
+ i18n.t("yes")
+ )}
{
className="btn btn-link btn-animate text-muted"
onClick={linkEvent(
this,
- this.handleModBanSubmit
+ this.handleBanPerson
)}
aria-label={i18n.t("unban_from_site")}
>
- {i18n.t("unban_from_site")}
+ {this.state.banLoading ? (
+
+ ) : (
+ i18n.t("unban_from_site")
+ )}
)}
>
@@ -804,7 +927,11 @@ export class CommentNode extends Component {
)}
aria-label={i18n.t("yes")}
>
- {i18n.t("yes")}
+ {this.state.addAdminLoading ? (
+
+ ) : (
+ i18n.t("yes")
+ )}
{
className="btn btn-link text-muted"
onClick={linkEvent(this, this.handleFetchChildren)}
>
- {i18n.t("x_more_replies", {
- count: node.comment_view.counts.child_count,
- formattedCount: numToSI(node.comment_view.counts.child_count),
- })}{" "}
- ➔
+ {this.state.fetchChildrenLoading ? (
+
+ ) : (
+ <>
+ {i18n.t("x_more_replies", {
+ count: node.comment_view.counts.child_count,
+ formattedCount: numToSI(
+ node.comment_view.counts.child_count
+ ),
+ })}{" "}
+ ➔
+ >
+ )}
)}
@@ -853,7 +988,7 @@ export class CommentNode extends Component {
{this.state.showRemoveDialog && (
)}
{this.state.showPurgeDialog && (
-
+
{i18n.t("reason")}
@@ -1008,9 +1149,13 @@ export class CommentNode extends Component {
node={node}
onReplyCancel={this.handleReplyCancel}
disabled={this.props.locked}
+ finished={this.props.finished.get(
+ this.props.node.comment_view.comment.id
+ )}
focus
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
+ onUpsertComment={this.props.onCreateComment}
/>
)}
{!this.state.collapsed && node.children.length > 0 && (
@@ -1024,6 +1169,26 @@ export class CommentNode extends Component {
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
hideImages={this.props.hideImages}
+ finished={this.props.finished}
+ onCommentReplyRead={this.props.onCommentReplyRead}
+ onPersonMentionRead={this.props.onPersonMentionRead}
+ onCreateComment={this.props.onCreateComment}
+ onEditComment={this.props.onEditComment}
+ onCommentVote={this.props.onCommentVote}
+ onBlockPerson={this.props.onBlockPerson}
+ onSaveComment={this.props.onSaveComment}
+ onDeleteComment={this.props.onDeleteComment}
+ onRemoveComment={this.props.onRemoveComment}
+ onDistinguishComment={this.props.onDistinguishComment}
+ onAddModToCommunity={this.props.onAddModToCommunity}
+ onAddAdmin={this.props.onAddAdmin}
+ onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
+ onBanPerson={this.props.onBanPerson}
+ onTransferCommunity={this.props.onTransferCommunity}
+ onFetchChildren={this.props.onFetchChildren}
+ onCommentReport={this.props.onCommentReport}
+ onPurgePerson={this.props.onPurgePerson}
+ onPurgeComment={this.props.onPurgeComment}
/>
)}
{/* A collapsed clearfix */}
@@ -1033,7 +1198,7 @@ export class CommentNode extends Component {
}
get commentReplyOrMentionRead(): boolean {
- let cv = this.props.node.comment_view;
+ const cv = this.commentView;
if (this.isPersonMentionType(cv)) {
return cv.person_mention.read;
@@ -1045,12 +1210,12 @@ export class CommentNode extends Component {
}
linkBtn(small = false) {
- let cv = this.props.node.comment_view;
- let classnames = classNames("btn btn-link btn-animate text-muted", {
+ const cv = this.commentView;
+ const classnames = classNames("btn btn-link btn-animate text-muted", {
"btn-sm": small,
});
- let title = this.props.showContext
+ const title = this.props.showContext
? i18n.t("show_context")
: i18n.t("link");
@@ -1075,26 +1240,52 @@ export class CommentNode extends Component {
);
}
- get loadingIcon() {
- return ;
- }
-
get myComment(): boolean {
return (
UserService.Instance.myUserInfo?.local_user_view.person.id ==
- this.props.node.comment_view.creator.id
+ this.commentView.creator.id
);
}
get isPostCreator(): boolean {
- return (
- this.props.node.comment_view.creator.id ==
- this.props.node.comment_view.post.creator_id
- );
+ return this.commentView.creator.id == this.commentView.post.creator_id;
+ }
+
+ get scoreColor() {
+ if (this.commentView.my_vote == 1) {
+ return "text-info";
+ } else if (this.commentView.my_vote == -1) {
+ return "text-danger";
+ } else {
+ return "text-muted";
+ }
+ }
+
+ get pointsTippy(): string {
+ const points = i18n.t("number_of_points", {
+ count: Number(this.commentView.counts.score),
+ formattedCount: numToSI(this.commentView.counts.score),
+ });
+
+ const upvotes = i18n.t("number_of_upvotes", {
+ count: Number(this.commentView.counts.upvotes),
+ formattedCount: numToSI(this.commentView.counts.upvotes),
+ });
+
+ const downvotes = i18n.t("number_of_downvotes", {
+ count: Number(this.commentView.counts.downvotes),
+ formattedCount: numToSI(this.commentView.counts.downvotes),
+ });
+
+ return `${points} • ${upvotes} • ${downvotes}`;
+ }
+
+ get expandText(): string {
+ return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");
}
get commentUnlessRemoved(): string {
- let comment = this.props.node.comment_view.comment;
+ const comment = this.commentView.comment;
return comment.removed
? `*${i18n.t("removed")}*`
: comment.deleted
@@ -1110,127 +1301,10 @@ export class CommentNode extends Component {
i.setState({ showEdit: true });
}
- handleBlockUserClick(i: CommentNode) {
- let auth = myAuth();
- if (auth) {
- let blockUserForm: BlockPerson = {
- person_id: i.props.node.comment_view.creator.id,
- block: true,
- auth,
- };
- WebSocketService.Instance.send(wsClient.blockPerson(blockUserForm));
- }
- }
-
- handleDeleteClick(i: CommentNode) {
- let comment = i.props.node.comment_view.comment;
- let auth = myAuth();
- if (auth) {
- let deleteForm: DeleteComment = {
- comment_id: comment.id,
- deleted: !comment.deleted,
- auth,
- };
- WebSocketService.Instance.send(wsClient.deleteComment(deleteForm));
- }
- }
-
- handleSaveCommentClick(i: CommentNode) {
- let cv = i.props.node.comment_view;
- let save = cv.saved == undefined ? true : !cv.saved;
- let auth = myAuth();
- if (auth) {
- let form: SaveComment = {
- comment_id: cv.comment.id,
- save,
- auth,
- };
-
- WebSocketService.Instance.send(wsClient.saveComment(form));
-
- i.setState({ saveLoading: true });
- }
- }
-
handleReplyCancel() {
this.setState({ showReply: false, showEdit: false });
}
- handleCommentUpvote(event: any) {
- event.preventDefault();
- let myVote = this.state.my_vote;
- let newVote = myVote == 1 ? 0 : 1;
-
- if (myVote == 1) {
- this.setState({
- score: this.state.score - 1,
- upvotes: this.state.upvotes - 1,
- });
- } else if (myVote == -1) {
- this.setState({
- downvotes: this.state.downvotes - 1,
- upvotes: this.state.upvotes + 1,
- score: this.state.score + 2,
- });
- } else {
- this.setState({
- score: this.state.score + 1,
- upvotes: this.state.upvotes + 1,
- });
- }
-
- this.setState({ my_vote: newVote });
-
- let auth = myAuth();
- if (auth) {
- let form: CreateCommentLike = {
- comment_id: this.props.node.comment_view.comment.id,
- score: newVote,
- auth,
- };
- WebSocketService.Instance.send(wsClient.likeComment(form));
- setupTippy();
- }
- }
-
- handleCommentDownvote(event: any) {
- event.preventDefault();
- let myVote = this.state.my_vote;
- let newVote = myVote == -1 ? 0 : -1;
-
- if (myVote == 1) {
- this.setState({
- downvotes: this.state.downvotes + 1,
- upvotes: this.state.upvotes - 1,
- score: this.state.score - 2,
- });
- } else if (myVote == -1) {
- this.setState({
- downvotes: this.state.downvotes - 1,
- score: this.state.score + 1,
- });
- } else {
- this.setState({
- downvotes: this.state.downvotes + 1,
- score: this.state.score - 1,
- });
- }
-
- this.setState({ my_vote: newVote });
-
- let auth = myAuth();
- if (auth) {
- let form: CreateCommentLike = {
- comment_id: this.props.node.comment_view.comment.id,
- score: newVote,
- auth,
- };
-
- WebSocketService.Instance.send(wsClient.likeComment(form));
- setupTippy();
- }
- }
-
handleShowReportDialog(i: CommentNode) {
i.setState({ showReportDialog: !i.state.showReportDialog });
}
@@ -1239,21 +1313,6 @@ export class CommentNode extends Component {
i.setState({ reportReason: event.target.value });
}
- handleReportSubmit(i: CommentNode) {
- let comment = i.props.node.comment_view.comment;
- let reason = i.state.reportReason;
- let auth = myAuth();
- if (reason && auth) {
- let form: CreateCommentReport = {
- comment_id: comment.id,
- reason,
- auth,
- };
- WebSocketService.Instance.send(wsClient.createCommentReport(form));
- i.setState({ showReportDialog: false });
- }
- }
-
handleModRemoveShow(i: CommentNode) {
i.setState({
showRemoveDialog: !i.state.showRemoveDialog,
@@ -1269,36 +1328,6 @@ export class CommentNode extends Component {
i.setState({ removeData: event.target.checked });
}
- handleModRemoveSubmit(i: CommentNode) {
- let comment = i.props.node.comment_view.comment;
- let auth = myAuth();
- if (auth) {
- let form: RemoveComment = {
- comment_id: comment.id,
- removed: !comment.removed,
- reason: i.state.removeReason,
- auth,
- };
- WebSocketService.Instance.send(wsClient.removeComment(form));
-
- i.setState({ showRemoveDialog: false });
- }
- }
-
- handleDistinguishClick(i: CommentNode) {
- let comment = i.props.node.comment_view.comment;
- let auth = myAuth();
- if (auth) {
- let form: DistinguishComment = {
- comment_id: comment.id,
- distinguished: !comment.distinguished,
- auth,
- };
- WebSocketService.Instance.send(wsClient.editComment(form));
- i.setState(i.state);
- }
- }
-
isPersonMentionType(
item: CommentView | PersonMentionView | CommentReplyView
): item is PersonMentionView {
@@ -1311,29 +1340,6 @@ export class CommentNode extends Component {
return (item as CommentReplyView).comment_reply?.id !== undefined;
}
- handleMarkRead(i: CommentNode) {
- let auth = myAuth();
- if (auth) {
- if (i.isPersonMentionType(i.props.node.comment_view)) {
- let form: MarkPersonMentionAsRead = {
- person_mention_id: i.props.node.comment_view.person_mention.id,
- read: !i.props.node.comment_view.person_mention.read,
- auth,
- };
- WebSocketService.Instance.send(wsClient.markPersonMentionAsRead(form));
- } else if (i.isCommentReplyType(i.props.node.comment_view)) {
- let form: MarkCommentReplyAsRead = {
- comment_reply_id: i.props.node.comment_view.comment_reply.id,
- read: !i.props.node.comment_view.comment_reply.read,
- auth,
- };
- WebSocketService.Instance.send(wsClient.markCommentReplyAsRead(form));
- }
-
- i.setState({ readLoading: true });
- }
- }
-
handleModBanFromCommunityShow(i: CommentNode) {
i.setState({
showBanDialog: true,
@@ -1358,57 +1364,6 @@ export class CommentNode extends Component {
i.setState({ banExpireDays: event.target.value });
}
- handleModBanFromCommunitySubmit(i: CommentNode) {
- i.setState({ banType: BanType.Community });
- i.handleModBanBothSubmit(i);
- }
-
- handleModBanSubmit(i: CommentNode) {
- i.setState({ banType: BanType.Site });
- i.handleModBanBothSubmit(i);
- }
-
- handleModBanBothSubmit(i: CommentNode) {
- let cv = i.props.node.comment_view;
- let auth = myAuth();
- if (auth) {
- if (i.state.banType == BanType.Community) {
- // If its an unban, restore all their data
- let ban = !cv.creator_banned_from_community;
- if (ban == false) {
- i.setState({ removeData: false });
- }
- let form: BanFromCommunity = {
- person_id: cv.creator.id,
- community_id: cv.community.id,
- ban,
- remove_data: i.state.removeData,
- reason: i.state.banReason,
- expires: futureDaysToUnixTime(i.state.banExpireDays),
- auth,
- };
- WebSocketService.Instance.send(wsClient.banFromCommunity(form));
- } else {
- // If its an unban, restore all their data
- let ban = !cv.creator.banned;
- if (ban == false) {
- i.setState({ removeData: false });
- }
- let form: BanPerson = {
- person_id: cv.creator.id,
- ban,
- remove_data: i.state.removeData,
- reason: i.state.banReason,
- expires: futureDaysToUnixTime(i.state.banExpireDays),
- auth,
- };
- WebSocketService.Instance.send(wsClient.banPerson(form));
- }
-
- i.setState({ showBanDialog: false });
- }
- }
-
handlePurgePersonShow(i: CommentNode) {
i.setState({
showPurgeDialog: true,
@@ -1429,30 +1384,6 @@ export class CommentNode extends Component {
i.setState({ purgeReason: event.target.value });
}
- handlePurgeSubmit(i: CommentNode, event: any) {
- event.preventDefault();
- let auth = myAuth();
- if (auth) {
- if (i.state.purgeType == PurgeType.Person) {
- let form: PurgePerson = {
- person_id: i.props.node.comment_view.creator.id,
- reason: i.state.purgeReason,
- auth,
- };
- WebSocketService.Instance.send(wsClient.purgePerson(form));
- } else if (i.state.purgeType == PurgeType.Comment) {
- let form: PurgeComment = {
- comment_id: i.props.node.comment_view.comment.id,
- reason: i.state.purgeReason,
- auth,
- };
- WebSocketService.Instance.send(wsClient.purgeComment(form));
- }
-
- i.setState({ purgeLoading: true });
- }
- }
-
handleShowConfirmAppointAsMod(i: CommentNode) {
i.setState({ showConfirmAppointAsMod: true });
}
@@ -1461,21 +1392,6 @@ export class CommentNode extends Component {
i.setState({ showConfirmAppointAsMod: false });
}
- handleAddModToCommunity(i: CommentNode) {
- let cv = i.props.node.comment_view;
- let auth = myAuth();
- if (auth) {
- let form: AddModToCommunity = {
- person_id: cv.creator.id,
- community_id: cv.community.id,
- added: !isMod(cv.creator.id, i.props.moderators),
- auth,
- };
- WebSocketService.Instance.send(wsClient.addModToCommunity(form));
- i.setState({ showConfirmAppointAsMod: false });
- }
- }
-
handleShowConfirmAppointAsAdmin(i: CommentNode) {
i.setState({ showConfirmAppointAsAdmin: true });
}
@@ -1484,20 +1400,6 @@ export class CommentNode extends Component {
i.setState({ showConfirmAppointAsAdmin: false });
}
- handleAddAdmin(i: CommentNode) {
- let auth = myAuth();
- if (auth) {
- let creatorId = i.props.node.comment_view.creator.id;
- let form: AddAdmin = {
- person_id: creatorId,
- added: !isAdmin(creatorId, i.props.admins),
- auth,
- };
- WebSocketService.Instance.send(wsClient.addAdmin(form));
- i.setState({ showConfirmAppointAsAdmin: false });
- }
- }
-
handleShowConfirmTransferCommunity(i: CommentNode) {
i.setState({ showConfirmTransferCommunity: true });
}
@@ -1506,20 +1408,6 @@ export class CommentNode extends Component {
i.setState({ showConfirmTransferCommunity: false });
}
- handleTransferCommunity(i: CommentNode) {
- let cv = i.props.node.comment_view;
- let auth = myAuth();
- if (auth) {
- let form: TransferCommunity = {
- community_id: cv.community.id,
- person_id: cv.creator.id,
- auth,
- };
- WebSocketService.Instance.send(wsClient.transferCommunity(form));
- i.setState({ showConfirmTransferCommunity: false });
- }
- }
-
handleShowConfirmTransferSite(i: CommentNode) {
i.setState({ showConfirmTransferSite: true });
}
@@ -1529,8 +1417,8 @@ export class CommentNode extends Component {
}
get isCommentNew(): boolean {
- let now = moment.utc().subtract(10, "minutes");
- let then = moment.utc(this.props.node.comment_view.comment.published);
+ const now = moment.utc().subtract(10, "minutes");
+ const then = moment.utc(this.commentView.comment.published);
return now.isBefore(then);
}
@@ -1548,50 +1436,193 @@ export class CommentNode extends Component {
setupTippy();
}
+ handleSaveComment(i: CommentNode) {
+ i.setState({ saveLoading: true });
+
+ i.props.onSaveComment({
+ comment_id: i.commentView.comment.id,
+ save: !i.commentView.saved,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleUpvote(i: CommentNode) {
+ i.setState({ upvoteLoading: true });
+ i.props.onCommentVote({
+ comment_id: i.commentId,
+ score: newVote(VoteType.Upvote, i.commentView.my_vote),
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleDownvote(i: CommentNode) {
+ i.setState({ downvoteLoading: true });
+ i.props.onCommentVote({
+ comment_id: i.commentId,
+ score: newVote(VoteType.Downvote, i.commentView.my_vote),
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleBlockPerson(i: CommentNode) {
+ i.setState({ blockPersonLoading: true });
+ i.props.onBlockPerson({
+ person_id: i.commentView.creator.id,
+ block: true,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleMarkAsRead(i: CommentNode) {
+ i.setState({ readLoading: true });
+ const cv = i.commentView;
+ if (i.isPersonMentionType(cv)) {
+ i.props.onPersonMentionRead({
+ person_mention_id: cv.person_mention.id,
+ read: !cv.person_mention.read,
+ auth: myAuthRequired(),
+ });
+ } else if (i.isCommentReplyType(cv)) {
+ i.props.onCommentReplyRead({
+ comment_reply_id: cv.comment_reply.id,
+ read: !cv.comment_reply.read,
+ auth: myAuthRequired(),
+ });
+ }
+ }
+
+ handleDeleteComment(i: CommentNode) {
+ i.setState({ deleteLoading: true });
+ i.props.onDeleteComment({
+ comment_id: i.commentId,
+ deleted: !i.commentView.comment.deleted,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleRemoveComment(i: CommentNode, event: any) {
+ event.preventDefault();
+ i.setState({ removeLoading: true });
+ i.props.onRemoveComment({
+ comment_id: i.commentId,
+ removed: !i.commentView.comment.removed,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleDistinguishComment(i: CommentNode) {
+ i.setState({ distinguishLoading: true });
+ i.props.onDistinguishComment({
+ comment_id: i.commentId,
+ distinguished: !i.commentView.comment.distinguished,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleBanPersonFromCommunity(i: CommentNode) {
+ i.setState({ banLoading: true });
+ i.props.onBanPersonFromCommunity({
+ community_id: i.commentView.community.id,
+ person_id: i.commentView.creator.id,
+ ban: !i.commentView.creator_banned_from_community,
+ reason: i.state.banReason,
+ remove_data: i.state.removeData,
+ expires: futureDaysToUnixTime(i.state.banExpireDays),
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleBanPerson(i: CommentNode) {
+ i.setState({ banLoading: true });
+ i.props.onBanPerson({
+ person_id: i.commentView.creator.id,
+ ban: !i.commentView.creator_banned_from_community,
+ reason: i.state.banReason,
+ remove_data: i.state.removeData,
+ expires: futureDaysToUnixTime(i.state.banExpireDays),
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleModBanBothSubmit(i: CommentNode, event: any) {
+ event.preventDefault();
+ if (i.state.banType == BanType.Community) {
+ i.handleBanPersonFromCommunity(i);
+ } else {
+ i.handleBanPerson(i);
+ }
+ }
+
+ handleAddModToCommunity(i: CommentNode) {
+ i.setState({ addModLoading: true });
+
+ const added = !isMod(i.commentView.comment.creator_id, i.props.moderators);
+ i.props.onAddModToCommunity({
+ community_id: i.commentView.community.id,
+ person_id: i.commentView.creator.id,
+ added,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleAddAdmin(i: CommentNode) {
+ i.setState({ addAdminLoading: true });
+
+ const added = !isAdmin(i.commentView.comment.creator_id, i.props.admins);
+ i.props.onAddAdmin({
+ person_id: i.commentView.creator.id,
+ added,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleTransferCommunity(i: CommentNode) {
+ i.setState({ transferCommunityLoading: true });
+ i.props.onTransferCommunity({
+ community_id: i.commentView.community.id,
+ person_id: i.commentView.creator.id,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleReportComment(i: CommentNode, event: any) {
+ event.preventDefault();
+ i.setState({ reportLoading: true });
+ i.props.onCommentReport({
+ comment_id: i.commentId,
+ reason: i.state.reportReason ?? "",
+ auth: myAuthRequired(),
+ });
+ }
+
+ handlePurgeBothSubmit(i: CommentNode, event: any) {
+ event.preventDefault();
+ i.setState({ purgeLoading: true });
+
+ if (i.state.purgeType == PurgeType.Person) {
+ i.props.onPurgePerson({
+ person_id: i.commentView.creator.id,
+ reason: i.state.purgeReason,
+ auth: myAuthRequired(),
+ });
+ } else {
+ i.props.onPurgeComment({
+ comment_id: i.commentId,
+ reason: i.state.purgeReason,
+ auth: myAuthRequired(),
+ });
+ }
+ }
+
handleFetchChildren(i: CommentNode) {
- let form: GetComments = {
- post_id: i.props.node.comment_view.post.id,
- parent_id: i.props.node.comment_view.comment.id,
+ i.setState({ fetchChildrenLoading: true });
+ i.props.onFetchChildren?.({
+ parent_id: i.commentId,
max_depth: commentTreeMaxDepth,
limit: 999, // TODO
type_: "All",
saved_only: false,
- auth: myAuth(false),
- };
-
- WebSocketService.Instance.send(wsClient.getComments(form));
- }
-
- get scoreColor() {
- if (this.state.my_vote == 1) {
- return "text-info";
- } else if (this.state.my_vote == -1) {
- return "text-danger";
- } else {
- return "text-muted";
- }
- }
-
- get pointsTippy(): string {
- let points = i18n.t("number_of_points", {
- count: Number(this.state.score),
- formattedCount: numToSI(this.state.score),
+ auth: myAuth(),
});
-
- let upvotes = i18n.t("number_of_upvotes", {
- count: Number(this.state.upvotes),
- formattedCount: numToSI(this.state.upvotes),
- });
-
- let downvotes = i18n.t("number_of_downvotes", {
- count: Number(this.state.downvotes),
- formattedCount: numToSI(this.state.downvotes),
- });
-
- return `${points} • ${upvotes} • ${downvotes}`;
- }
-
- get expandText(): string {
- return this.state.collapsed ? i18n.t("expand") : i18n.t("collapse");
}
}
diff --git a/src/shared/components/comment/comment-nodes.tsx b/src/shared/components/comment/comment-nodes.tsx
index ba8997ac..3f9b48ef 100644
--- a/src/shared/components/comment/comment-nodes.tsx
+++ b/src/shared/components/comment/comment-nodes.tsx
@@ -1,5 +1,29 @@
import { Component } from "inferno";
-import { CommunityModeratorView, Language, PersonView } from "lemmy-js-client";
+import {
+ AddAdmin,
+ AddModToCommunity,
+ BanFromCommunity,
+ BanPerson,
+ BlockPerson,
+ CommentId,
+ CommunityModeratorView,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ DeleteComment,
+ DistinguishComment,
+ EditComment,
+ GetComments,
+ Language,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
+ PersonView,
+ PurgeComment,
+ PurgePerson,
+ RemoveComment,
+ SaveComment,
+ TransferCommunity,
+} from "lemmy-js-client";
import { CommentNodeI, CommentViewType } from "../../interfaces";
import { CommentNode } from "./comment-node";
@@ -20,6 +44,26 @@ interface CommentNodesProps {
allLanguages: Language[];
siteLanguages: number[];
hideImages?: boolean;
+ finished: Map;
+ onSaveComment(form: SaveComment): void;
+ onCommentReplyRead(form: MarkCommentReplyAsRead): void;
+ onPersonMentionRead(form: MarkPersonMentionAsRead): void;
+ onCreateComment(form: EditComment | CreateComment): void;
+ onEditComment(form: EditComment | CreateComment): void;
+ onCommentVote(form: CreateCommentLike): void;
+ onBlockPerson(form: BlockPerson): void;
+ onDeleteComment(form: DeleteComment): void;
+ onRemoveComment(form: RemoveComment): void;
+ onDistinguishComment(form: DistinguishComment): void;
+ onAddModToCommunity(form: AddModToCommunity): void;
+ onAddAdmin(form: AddAdmin): void;
+ onBanPersonFromCommunity(form: BanFromCommunity): void;
+ onBanPerson(form: BanPerson): void;
+ onTransferCommunity(form: TransferCommunity): void;
+ onFetchChildren?(form: GetComments): void;
+ onCommentReport(form: CreateCommentReport): void;
+ onPurgePerson(form: PurgePerson): void;
+ onPurgeComment(form: PurgeComment): void;
}
export class CommentNodes extends Component {
@@ -28,7 +72,7 @@ export class CommentNodes extends Component {
}
render() {
- let maxComments = this.props.maxCommentsShown ?? this.props.nodes.length;
+ const maxComments = this.props.maxCommentsShown ?? this.props.nodes.length;
return (
@@ -50,6 +94,26 @@ export class CommentNodes extends Component {
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
hideImages={this.props.hideImages}
+ onCommentReplyRead={this.props.onCommentReplyRead}
+ onPersonMentionRead={this.props.onPersonMentionRead}
+ finished={this.props.finished}
+ onCreateComment={this.props.onCreateComment}
+ onEditComment={this.props.onEditComment}
+ onCommentVote={this.props.onCommentVote}
+ onBlockPerson={this.props.onBlockPerson}
+ onSaveComment={this.props.onSaveComment}
+ onDeleteComment={this.props.onDeleteComment}
+ onRemoveComment={this.props.onRemoveComment}
+ onDistinguishComment={this.props.onDistinguishComment}
+ onAddModToCommunity={this.props.onAddModToCommunity}
+ onAddAdmin={this.props.onAddAdmin}
+ onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
+ onBanPerson={this.props.onBanPerson}
+ onTransferCommunity={this.props.onTransferCommunity}
+ onFetchChildren={this.props.onFetchChildren}
+ onCommentReport={this.props.onCommentReport}
+ onPurgePerson={this.props.onPurgePerson}
+ onPurgeComment={this.props.onPurgeComment}
/>
))}
diff --git a/src/shared/components/comment/comment-report.tsx b/src/shared/components/comment/comment-report.tsx
index 232fec60..ff00bc59 100644
--- a/src/shared/components/comment/comment-report.tsx
+++ b/src/shared/components/comment/comment-report.tsx
@@ -1,4 +1,4 @@
-import { Component, linkEvent } from "inferno";
+import { Component, InfernoNode, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
CommentReportView,
@@ -7,32 +7,50 @@ import {
} from "lemmy-js-client";
import { i18n } from "../../i18next";
import { CommentNodeI, CommentViewType } from "../../interfaces";
-import { WebSocketService } from "../../services";
-import { myAuth, wsClient } from "../../utils";
-import { Icon } from "../common/icon";
+import { myAuthRequired } from "../../utils";
+import { Icon, Spinner } from "../common/icon";
import { PersonListing } from "../person/person-listing";
import { CommentNode } from "./comment-node";
interface CommentReportProps {
report: CommentReportView;
+ onResolveReport(form: ResolveCommentReport): void;
}
-export class CommentReport extends Component {
+interface CommentReportState {
+ loading: boolean;
+}
+
+export class CommentReport extends Component<
+ CommentReportProps,
+ CommentReportState
+> {
+ state: CommentReportState = {
+ loading: false,
+ };
constructor(props: any, context: any) {
super(props, context);
}
+ componentWillReceiveProps(
+ nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps>
+ ): void {
+ if (this.props != nextProps) {
+ this.setState({ loading: false });
+ }
+ }
+
render() {
- let r = this.props.report;
- let comment = r.comment;
- let tippyContent = i18n.t(
+ const r = this.props.report;
+ const comment = r.comment;
+ const tippyContent = i18n.t(
r.comment_report.resolved ? "unresolve_report" : "resolve_report"
);
// Set the original post data ( a troll could change it )
comment.content = r.comment_report.original_comment_text;
- let comment_view: CommentView = {
+ const comment_view: CommentView = {
comment,
creator: r.comment_creator,
post: r.post,
@@ -45,7 +63,7 @@ export class CommentReport extends Component {
my_vote: r.my_vote,
};
- let node: CommentNodeI = {
+ const node: CommentNodeI = {
comment_view,
children: [],
depth: 0,
@@ -62,6 +80,26 @@ export class CommentReport extends Component {
allLanguages={[]}
siteLanguages={[]}
hideImages
+ // All of these are unused, since its viewonly
+ finished={new Map()}
+ onSaveComment={() => {}}
+ onBlockPerson={() => {}}
+ onDeleteComment={() => {}}
+ onRemoveComment={() => {}}
+ onCommentVote={() => {}}
+ onCommentReport={() => {}}
+ onDistinguishComment={() => {}}
+ onAddModToCommunity={() => {}}
+ onAddAdmin={() => {}}
+ onTransferCommunity={() => {}}
+ onPurgeComment={() => {}}
+ onPurgePerson={() => {}}
+ onCommentReplyRead={() => {}}
+ onPersonMentionRead={() => {}}
+ onBanPersonFromCommunity={() => {}}
+ onBanPerson={() => {}}
+ onCreateComment={() => Promise.resolve({ state: "empty" })}
+ onEditComment={() => Promise.resolve({ state: "empty" })}
/>
{i18n.t("reporter")}:
@@ -90,26 +128,27 @@ export class CommentReport extends Component
{
data-tippy-content={tippyContent}
aria-label={tippyContent}
>
-
+ {this.state.loading ? (
+
+ ) : (
+
+ )}
);
}
handleResolveReport(i: CommentReport) {
- let auth = myAuth();
- if (auth) {
- let form: ResolveCommentReport = {
- report_id: i.props.report.comment_report.id,
- resolved: !i.props.report.comment_report.resolved,
- auth,
- };
- WebSocketService.Instance.send(wsClient.resolveCommentReport(form));
- }
+ i.setState({ loading: true });
+ i.props.onResolveReport({
+ report_id: i.props.report.comment_report.id,
+ resolved: !i.props.report.comment_report.resolved,
+ auth: myAuthRequired(),
+ });
}
}
diff --git a/src/shared/components/common/banner-icon-header.tsx b/src/shared/components/common/banner-icon-header.tsx
index e2901452..1df23bef 100644
--- a/src/shared/components/common/banner-icon-header.tsx
+++ b/src/shared/components/common/banner-icon-header.tsx
@@ -12,8 +12,8 @@ export class BannerIconHeader extends Component {
}
render() {
- let banner = this.props.banner;
- let icon = this.props.icon;
+ const banner = this.props.banner;
+ const icon = this.props.icon;
return (
{banner &&
}
diff --git a/src/shared/components/common/emoji-mart.tsx b/src/shared/components/common/emoji-mart.tsx
index 6210366b..dff8c3ac 100644
--- a/src/shared/components/common/emoji-mart.tsx
+++ b/src/shared/components/common/emoji-mart.tsx
@@ -12,7 +12,7 @@ export class EmojiMart extends Component
{
this.handleEmojiClick = this.handleEmojiClick.bind(this);
}
componentDidMount() {
- let div: any = document.getElementById("emoji-picker");
+ const div: any = document.getElementById("emoji-picker");
if (div) {
div.appendChild(
getEmojiMart(this.handleEmojiClick, this.props.pickerOptions)
diff --git a/src/shared/components/common/html-tags.tsx b/src/shared/components/common/html-tags.tsx
index 67abe3a7..0e6cb2d0 100644
--- a/src/shared/components/common/html-tags.tsx
+++ b/src/shared/components/common/html-tags.tsx
@@ -2,7 +2,7 @@ import { htmlToText } from "html-to-text";
import { Component } from "inferno";
import { Helmet } from "inferno-helmet";
import { httpExternalPath } from "../../env";
-import { md } from "../../utils";
+import { getLanguages, md } from "../../utils";
interface HtmlTagsProps {
title: string;
@@ -14,12 +14,15 @@ interface HtmlTagsProps {
/// Taken from https://metatags.io/
export class HtmlTags extends Component {
render() {
- let url = httpExternalPath(this.props.path);
- let desc = this.props.description;
- let image = this.props.image;
+ const url = httpExternalPath(this.props.path);
+ const desc = this.props.description;
+ const image = this.props.image;
+ const lang = getLanguages()[0];
return (
+
+
{["title", "og:title", "twitter:title"].map(t => (
))}
diff --git a/src/shared/components/common/image-upload-form.tsx b/src/shared/components/common/image-upload-form.tsx
index cfb86150..61fdd610 100644
--- a/src/shared/components/common/image-upload-form.tsx
+++ b/src/shared/components/common/image-upload-form.tsx
@@ -1,7 +1,7 @@
import { Component, linkEvent } from "inferno";
import { i18n } from "../../i18next";
-import { UserService } from "../../services";
-import { randomStr, toast, uploadImage } from "../../utils";
+import { HttpService, UserService } from "../../services";
+import { randomStr, toast } from "../../utils";
import { Icon } from "./icon";
interface ImageUploadFormProps {
@@ -73,27 +73,26 @@ export class ImageUploadForm extends Component<
handleImageUpload(i: ImageUploadForm, event: any) {
event.preventDefault();
- const file = event.target.files[0];
+ const image = event.target.files[0] as File;
i.setState({ loading: true });
- uploadImage(file)
- .then(res => {
- console.log("pictrs upload:");
- console.log(res);
- if (res.msg === "ok") {
- i.setState({ loading: false });
- i.props.onUpload(res.url as string);
+ HttpService.client.uploadImage({ image }).then(res => {
+ console.log("pictrs upload:");
+ console.log(res);
+ if (res.state === "success") {
+ if (res.data.msg === "ok") {
+ i.props.onUpload(res.data.url as string);
} else {
- i.setState({ loading: false });
toast(JSON.stringify(res), "danger");
}
- })
- .catch(error => {
- i.setState({ loading: false });
- console.error(error);
- toast(error, "danger");
- });
+ } else if (res.state === "failed") {
+ console.error(res.msg);
+ toast(res.msg, "danger");
+ }
+
+ i.setState({ loading: false });
+ });
}
handleRemoveImage(i: ImageUploadForm, event: any) {
diff --git a/src/shared/components/common/language-select.tsx b/src/shared/components/common/language-select.tsx
index a63c4db3..fac3216f 100644
--- a/src/shared/components/common/language-select.tsx
+++ b/src/shared/components/common/language-select.tsx
@@ -16,6 +16,7 @@ interface LanguageSelectProps {
showSite?: boolean;
iconVersion?: boolean;
disabled?: boolean;
+ showLanguageWarning?: boolean;
}
export class LanguageSelect extends Component {
@@ -31,12 +32,12 @@ export class LanguageSelect extends Component {
// Necessary because there is no HTML way to set selected for multiple in value=
setSelectedValues() {
- let ids = this.props.selectedLanguageIds?.map(toString);
+ const ids = this.props.selectedLanguageIds?.map(toString);
if (ids) {
- let select = (document.getElementById(this.id) as HTMLSelectElement)
+ const select = (document.getElementById(this.id) as HTMLSelectElement)
.options;
for (let i = 0; i < select.length; i++) {
- let o = select[i];
+ const o = select[i];
if (ids.includes(o.value)) {
o.selected = true;
}
@@ -49,7 +50,7 @@ export class LanguageSelect extends Component {
this.selectBtn
) : (
- {this.props.multiple && (
+ {this.props.multiple && this.props.showLanguageWarning && (
{i18n.t("undetermined_language_warning")}
@@ -107,7 +108,7 @@ export class LanguageSelect extends Component
{
)}
id={this.id}
onChange={linkEvent(this, this.handleLanguageChange)}
- aria-label="action"
+ aria-label={i18n.t("language_select_placeholder")}
multiple={this.props.multiple}
disabled={this.props.disabled}
>
@@ -130,8 +131,8 @@ export class LanguageSelect extends Component {
}
handleLanguageChange(i: LanguageSelect, event: any) {
- let options: HTMLOptionElement[] = Array.from(event.target.options);
- let selected: number[] = options
+ const options: HTMLOptionElement[] = Array.from(event.target.options);
+ const selected: number[] = options
.filter(o => o.selected)
.map(o => Number(o.value));
diff --git a/src/shared/components/common/listing-type-select.tsx b/src/shared/components/common/listing-type-select.tsx
index abafe375..3e534d34 100644
--- a/src/shared/components/common/listing-type-select.tsx
+++ b/src/shared/components/common/listing-type-select.tsx
@@ -8,7 +8,7 @@ interface ListingTypeSelectProps {
type_: ListingType;
showLocal: boolean;
showSubscribed: boolean;
- onChange?(val: ListingType): any;
+ onChange(val: ListingType): void;
}
interface ListingTypeSelectState {
@@ -29,11 +29,11 @@ export class ListingTypeSelect extends Component<
super(props, context);
}
- static getDerivedStateFromProps(props: any): ListingTypeSelectProps {
+ static getDerivedStateFromProps(
+ props: ListingTypeSelectProps
+ ): ListingTypeSelectState {
return {
type_: props.type_,
- showLocal: props.showLocal,
- showSubscribed: props.showSubscribed,
};
}
@@ -97,6 +97,6 @@ export class ListingTypeSelect extends Component<
}
handleTypeChange(i: ListingTypeSelect, event: any) {
- i.props.onChange?.(event.target.value);
+ i.props.onChange(event.target.value);
}
}
diff --git a/src/shared/components/common/markdown-textarea.tsx b/src/shared/components/common/markdown-textarea.tsx
index 8e65a0c3..9318d3bb 100644
--- a/src/shared/components/common/markdown-textarea.tsx
+++ b/src/shared/components/common/markdown-textarea.tsx
@@ -1,10 +1,9 @@
import autosize from "autosize";
import { NoOptionI18nKeys } from "i18next";
import { Component, linkEvent } from "inferno";
-import { Prompt } from "inferno-router";
import { Language } from "lemmy-js-client";
import { i18n } from "../../i18next";
-import { UserService } from "../../services";
+import { HttpService, UserService } from "../../services";
import {
concurrentImageUpload,
customEmojisLookup,
@@ -20,11 +19,11 @@ import {
setupTippy,
setupTribute,
toast,
- uploadImage,
} from "../../utils";
import { EmojiPicker } from "./emoji-picker";
import { Icon, Spinner } from "./icon";
import { LanguageSelect } from "./language-select";
+import NavigationPrompt from "./navigation-prompt";
import ProgressBar from "./progress-bar";
interface MarkdownTextAreaProps {
@@ -39,9 +38,9 @@ interface MarkdownTextAreaProps {
finished?: boolean;
showLanguage?: boolean;
hideNavigationWarnings?: boolean;
- onContentChange?(val: string): any;
- onReplyCancel?(): any;
- onSubmit?(msg: { val?: string; formId: string; languageId?: number }): any;
+ onContentChange?(val: string): void;
+ onReplyCancel?(): void;
+ onSubmit?(content: string, formId: string, languageId?: number): void;
allLanguages: Language[]; // TODO should probably be nullable
siteLanguages: number[]; // TODO same
}
@@ -55,8 +54,9 @@ interface MarkdownTextAreaState {
content?: string;
languageId?: number;
previewMode: boolean;
- loading: boolean;
imageUploadStatus?: ImageUploadStatus;
+ loading: boolean;
+ submitted: boolean;
}
export class MarkdownTextArea extends Component<
@@ -72,6 +72,7 @@ export class MarkdownTextArea extends Component<
languageId: this.props.initialLanguageId,
previewMode: false,
loading: false,
+ submitted: false,
};
constructor(props: any, context: any) {
@@ -85,7 +86,7 @@ export class MarkdownTextArea extends Component<
}
componentDidMount() {
- let textarea: any = document.getElementById(this.id);
+ const textarea: any = document.getElementById(this.id);
if (textarea) {
autosize(textarea);
this.tribute.attach(textarea);
@@ -105,17 +106,14 @@ export class MarkdownTextArea extends Component<
}
}
- componentDidUpdate() {
- if (!this.props.hideNavigationWarnings && this.state.content) {
- window.onbeforeunload = () => true;
- } else {
- window.onbeforeunload = null;
- }
- }
-
componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
if (nextProps.finished) {
- this.setState({ previewMode: false, loading: false, content: undefined });
+ this.setState({
+ previewMode: false,
+ imageUploadStatus: undefined,
+ loading: false,
+ content: undefined,
+ });
if (this.props.replyType) {
this.props.onReplyCancel?.();
}
@@ -127,18 +125,22 @@ export class MarkdownTextArea extends Component<
}
}
- componentWillUnmount() {
- window.onbeforeunload = null;
- }
-
render() {
- let languageId = this.state.languageId;
+ const languageId = this.state.languageId;
+ // TODO add these prompts back in at some point
+ //
return (
-
@@ -148,6 +150,7 @@ export class MarkdownTextArea extends Component<
value={this.state.content}
onInput={linkEvent(this, this.handleContentChange)}
onPaste={linkEvent(this, this.handleImageUploadPaste)}
+ onKeyDown={linkEvent(this, this.handleKeyBinds)}
required
disabled={this.isDisabled}
rows={2}
@@ -210,7 +213,7 @@ export class MarkdownTextArea extends Component<
}`}
onClick={linkEvent(this, this.handlePreviewToggle)}
>
- {i18n.t("preview")}
+ {this.state.previewMode ? i18n.t("edit") : i18n.t("preview")}
)}
{/* A flex expander */}
@@ -321,7 +324,7 @@ export class MarkdownTextArea extends Component<
handleEmoji(i: MarkdownTextArea, e: any) {
let value = e.native;
if (value == null) {
- let emoji = customEmojisLookup.get(e.id)?.custom_emoji;
+ const emoji = customEmojisLookup.get(e.id)?.custom_emoji;
if (emoji) {
value = `![${emoji.alt_text}](${emoji.image_url} "${emoji.shortcode}")`;
}
@@ -330,7 +333,7 @@ export class MarkdownTextArea extends Component<
content: `${i.state.content ?? ""} ${value} `,
});
i.contentChange();
- let textarea: any = document.getElementById(i.id);
+ const textarea: any = document.getElementById(i.id);
autosize.update(textarea);
}
@@ -392,35 +395,35 @@ export class MarkdownTextArea extends Component<
}
}
- async uploadSingleImage(i: MarkdownTextArea, file: File) {
- try {
- const res = await uploadImage(file);
- console.log("pictrs upload:");
- console.log(res);
- if (res.msg === "ok") {
- const imageMarkdown = `![](${res.url})`;
+ async uploadSingleImage(i: MarkdownTextArea, image: File) {
+ const res = await HttpService.client.uploadImage({ image });
+ console.log("pictrs upload:");
+ console.log(res);
+ if (res.state === "success") {
+ if (res.data.msg === "ok") {
+ const imageMarkdown = `![](${res.data.url})`;
i.setState(({ content }) => ({
content: content ? `${content}\n${imageMarkdown}` : imageMarkdown,
}));
i.contentChange();
const textarea: any = document.getElementById(i.id);
autosize.update(textarea);
- pictrsDeleteToast(file.name, res.delete_url as string);
+ pictrsDeleteToast(image.name, res.data.delete_url as string);
} else {
- throw JSON.stringify(res);
+ throw JSON.stringify(res.data);
}
- } catch (error) {
+ } else if (res.state === "failed") {
i.setState({ imageUploadStatus: undefined });
- console.error(error);
- toast(error, "danger");
+ console.error(res.msg);
+ toast(res.msg, "danger");
- throw error;
+ throw res.msg;
}
}
contentChange() {
// Coerces the undefineds to empty strings, for replacing in the DB
- let content = this.state.content ?? "";
+ const content = this.state.content ?? "";
this.props.onContentChange?.(content);
}
@@ -429,6 +432,54 @@ export class MarkdownTextArea extends Component<
i.contentChange();
}
+ // Keybind handler
+ // Keybinds inspired by github comment area
+ handleKeyBinds(i: MarkdownTextArea, event: KeyboardEvent) {
+ if (event.ctrlKey) {
+ switch (event.key) {
+ case "k": {
+ i.handleInsertLink(i, event);
+ break;
+ }
+ case "Enter": {
+ if (!this.isDisabled) {
+ i.handleSubmit(i, event);
+ }
+
+ break;
+ }
+ case "b": {
+ i.handleInsertBold(i, event);
+ break;
+ }
+ case "i": {
+ i.handleInsertItalic(i, event);
+ break;
+ }
+ case "e": {
+ i.handleInsertCode(i, event);
+ break;
+ }
+ case "8": {
+ i.handleInsertList(i, event);
+ break;
+ }
+ case "s": {
+ i.handleInsertSpoiler(i, event);
+ break;
+ }
+ case "p": {
+ if (i.state.content) i.handlePreviewToggle(i, event);
+ break;
+ }
+ case ".": {
+ i.handleInsertQuote(i, event);
+ break;
+ }
+ }
+ }
+ }
+
handlePreviewToggle(i: MarkdownTextArea, event: any) {
event.preventDefault();
i.setState({ previewMode: !i.state.previewMode });
@@ -440,13 +491,10 @@ export class MarkdownTextArea extends Component<
handleSubmit(i: MarkdownTextArea, event: any) {
event.preventDefault();
- i.setState({ loading: true });
- let msg = {
- val: i.state.content,
- formId: i.formId,
- languageId: i.state.languageId,
- };
- i.props.onSubmit?.(msg);
+ if (i.state.content) {
+ i.setState({ loading: true, submitted: true });
+ i.props.onSubmit?.(i.state.content, i.formId, i.state.languageId);
+ }
}
handleReplyCancel(i: MarkdownTextArea) {
@@ -565,7 +613,7 @@ export class MarkdownTextArea extends Component<
handleInsertList(i: MarkdownTextArea, event: any) {
event.preventDefault();
- i.simpleBeginningofLine("-");
+ i.simpleBeginningofLine(`-${i.getSelectedText() ? " " : ""}`);
}
handleInsertQuote(i: MarkdownTextArea, event: any) {
@@ -589,7 +637,7 @@ export class MarkdownTextArea extends Component<
}
simpleInsert(chars: string) {
- let content = this.state.content;
+ const content = this.state.content;
if (!content) {
this.setState({ content: `${chars} ` });
} else {
@@ -598,7 +646,7 @@ export class MarkdownTextArea extends Component<
});
}
- let textarea: any = document.getElementById(this.id);
+ const textarea: any = document.getElementById(this.id);
textarea.focus();
setTimeout(() => {
autosize.update(textarea);
@@ -608,8 +656,8 @@ export class MarkdownTextArea extends Component<
handleInsertSpoiler(i: MarkdownTextArea, event: any) {
event.preventDefault();
- let beforeChars = `\n::: spoiler ${i18n.t("spoiler")}\n`;
- let afterChars = "\n:::\n";
+ const beforeChars = `\n::: spoiler ${i18n.t("spoiler")}\n`;
+ const afterChars = "\n:::\n";
i.simpleSurroundBeforeAfter(beforeChars, afterChars);
}
diff --git a/src/shared/components/common/moment-time.tsx b/src/shared/components/common/moment-time.tsx
index 81aedd1d..10714f5b 100644
--- a/src/shared/components/common/moment-time.tsx
+++ b/src/shared/components/common/moment-time.tsx
@@ -15,13 +15,13 @@ export class MomentTime extends Component
{
constructor(props: any, context: any) {
super(props, context);
- let lang = getLanguages();
+ const lang = getLanguages();
moment.locale(lang);
}
createdAndModifiedTimes() {
- let updated = this.props.updated;
+ const updated = this.props.updated;
let line = `${capitalizeFirstLetter(i18n.t("created"))}: ${this.format(
this.props.published
)}`;
@@ -45,7 +45,7 @@ export class MomentTime extends Component {
);
} else {
- let published = this.props.published;
+ const published = this.props.published;
return (
{
+ public unblock;
+
+ public enable() {
+ if (this.unblock) {
+ this.unblock();
+ }
+
+ this.unblock = this.context.router.history.block(tx => {
+ if (window.confirm(i18n.t("block_leaving"))) {
+ this.unblock();
+ tx.retry();
+ }
+ });
+ }
+
+ public disable() {
+ if (this.unblock) {
+ this.unblock();
+ this.unblock = null;
+ }
+ }
+ public componentWillMount() {
+ if (this.props.when) {
+ this.enable();
+ }
+ }
+
+ public componentWillReceiveProps(nextProps: IPromptProps) {
+ if (nextProps.when) {
+ if (!this.props.when) {
+ this.enable();
+ }
+ } else {
+ this.disable();
+ }
+ }
+
+ public componentWillUnmount() {
+ this.disable();
+ }
+
+ public render() {
+ return null;
+ }
+}
diff --git a/src/shared/components/common/pictrs-image.tsx b/src/shared/components/common/pictrs-image.tsx
index 4f3ddc05..27d1cc5f 100644
--- a/src/shared/components/common/pictrs-image.tsx
+++ b/src/shared/components/common/pictrs-image.tsx
@@ -29,6 +29,7 @@ export class PictrsImage extends Component {
{
"img-expanded slight-radius":
!this.props.thumbnail && !this.props.icon,
"img-blur": this.props.thumbnail && this.props.nsfw,
- "rounded-circle img-icon mr-2": this.props.icon,
- "ml-2 mb-0 rounded-circle avatar-overlay": this.props.iconOverlay,
+ "rounded-circle img-cover img-icon mr-2": this.props.icon,
+ "ml-2 mb-0 rounded-circle img-cover avatar-overlay":
+ this.props.iconOverlay,
"avatar-pushup": this.props.pushup,
})}
/>
@@ -51,17 +53,17 @@ export class PictrsImage extends Component {
// sample url:
// http://localhost:8535/pictrs/image/file.png?thumbnail=256&format=jpg
- let split = this.props.src.split("/pictrs/image/");
+ const split = this.props.src.split("/pictrs/image/");
// If theres not multiple, then its not a pictrs image
if (split.length == 1) {
return this.props.src;
}
- let host = split[0];
- let path = split[1];
+ const host = split[0];
+ const path = split[1];
- let params = { format };
+ const params = { format };
if (this.props.thumbnail) {
params["thumbnail"] = thumbnailSize;
@@ -69,8 +71,8 @@ export class PictrsImage extends Component {
params["thumbnail"] = iconThumbnailSize;
}
- let paramsStr = new URLSearchParams(params).toString();
- let out = `${host}/pictrs/image/${path}?${paramsStr}`;
+ const paramsStr = new URLSearchParams(params).toString();
+ const out = `${host}/pictrs/image/${path}?${paramsStr}`;
return out;
}
diff --git a/src/shared/components/common/progress-bar.tsx b/src/shared/components/common/progress-bar.tsx
index 5ddc5ca6..88aee7ed 100644
--- a/src/shared/components/common/progress-bar.tsx
+++ b/src/shared/components/common/progress-bar.tsx
@@ -1,5 +1,5 @@
import classNames from "classnames";
-import { ThemeColor } from "shared/utils";
+import { ThemeColor } from "../../utils";
interface ProgressBarProps {
className?: string;
diff --git a/src/shared/components/common/registration-application.tsx b/src/shared/components/common/registration-application.tsx
index cbd8ffae..503892c3 100644
--- a/src/shared/components/common/registration-application.tsx
+++ b/src/shared/components/common/registration-application.tsx
@@ -1,23 +1,26 @@
-import { Component, linkEvent } from "inferno";
+import { Component, InfernoNode, linkEvent } from "inferno";
import { T } from "inferno-i18next-dess";
import {
ApproveRegistrationApplication,
RegistrationApplicationView,
} from "lemmy-js-client";
import { i18n } from "../../i18next";
-import { WebSocketService } from "../../services";
-import { mdToHtml, myAuth, wsClient } from "../../utils";
+import { mdToHtml, myAuthRequired } from "../../utils";
import { PersonListing } from "../person/person-listing";
+import { Spinner } from "./icon";
import { MarkdownTextArea } from "./markdown-textarea";
import { MomentTime } from "./moment-time";
interface RegistrationApplicationProps {
application: RegistrationApplicationView;
+ onApproveApplication(form: ApproveRegistrationApplication): void;
}
interface RegistrationApplicationState {
denyReason?: string;
denyExpanded: boolean;
+ approveLoading: boolean;
+ denyLoading: boolean;
}
export class RegistrationApplication extends Component<
@@ -27,17 +30,32 @@ export class RegistrationApplication extends Component<
state: RegistrationApplicationState = {
denyReason: this.props.application.registration_application.deny_reason,
denyExpanded: false,
+ approveLoading: false,
+ denyLoading: false,
};
constructor(props: any, context: any) {
super(props, context);
this.handleDenyReasonChange = this.handleDenyReasonChange.bind(this);
}
+ componentWillReceiveProps(
+ nextProps: Readonly<
+ { children?: InfernoNode } & RegistrationApplicationProps
+ >
+ ): void {
+ if (this.props != nextProps) {
+ this.setState({
+ denyExpanded: false,
+ approveLoading: false,
+ denyLoading: false,
+ });
+ }
+ }
render() {
- let a = this.props.application;
- let ra = this.props.application.registration_application;
- let accepted = a.creator_local_user.accepted_application;
+ const a = this.props.application;
+ const ra = this.props.application.registration_application;
+ const accepted = a.creator_local_user.accepted_application;
return (
@@ -99,7 +117,7 @@ export class RegistrationApplication extends Component<
onClick={linkEvent(this, this.handleApprove)}
aria-label={i18n.t("approve")}
>
- {i18n.t("approve")}
+ {this.state.approveLoading ? : i18n.t("approve")}
)}
{(!ra.admin_id || (ra.admin_id && accepted)) && (
@@ -108,7 +126,7 @@ export class RegistrationApplication extends Component<
onClick={linkEvent(this, this.handleDeny)}
aria-label={i18n.t("deny")}
>
- {i18n.t("deny")}
+ {this.state.denyLoading ? : i18n.t("deny")}
)}
@@ -116,35 +134,23 @@ export class RegistrationApplication extends Component<
}
handleApprove(i: RegistrationApplication) {
- let auth = myAuth();
- if (auth) {
- i.setState({ denyExpanded: false });
- let form: ApproveRegistrationApplication = {
- id: i.props.application.registration_application.id,
- approve: true,
- auth,
- };
- WebSocketService.Instance.send(
- wsClient.approveRegistrationApplication(form)
- );
- }
+ i.setState({ denyExpanded: false, approveLoading: true });
+ i.props.onApproveApplication({
+ id: i.props.application.registration_application.id,
+ approve: true,
+ auth: myAuthRequired(),
+ });
}
handleDeny(i: RegistrationApplication) {
if (i.state.denyExpanded) {
- i.setState({ denyExpanded: false });
- let auth = myAuth();
- if (auth) {
- let form: ApproveRegistrationApplication = {
- id: i.props.application.registration_application.id,
- approve: false,
- deny_reason: i.state.denyReason,
- auth,
- };
- WebSocketService.Instance.send(
- wsClient.approveRegistrationApplication(form)
- );
- }
+ i.setState({ denyExpanded: false, denyLoading: true });
+ i.props.onApproveApplication({
+ id: i.props.application.registration_application.id,
+ approve: false,
+ deny_reason: i.state.denyReason,
+ auth: myAuthRequired(),
+ });
} else {
i.setState({ denyExpanded: true });
}
diff --git a/src/shared/components/common/searchable-select.tsx b/src/shared/components/common/searchable-select.tsx
index a5a75f23..cd630367 100644
--- a/src/shared/components/common/searchable-select.tsx
+++ b/src/shared/components/common/searchable-select.tsx
@@ -38,12 +38,38 @@ function handleSearch(i: SearchableSelect, e: ChangeEvent) {
});
}
+function focusSearch(i: SearchableSelect) {
+ if (i.toggleButtonRef.current?.ariaExpanded !== "true") {
+ i.searchInputRef.current?.focus();
+
+ if (i.props.onSearch) {
+ i.props.onSearch("");
+ }
+
+ i.setState({
+ searchText: "",
+ });
+ }
+}
+
+function handleChange({ option, i }: { option: Choice; i: SearchableSelect }) {
+ const { onChange, value } = i.props;
+
+ if (option.value !== value?.toString()) {
+ if (onChange) {
+ onChange(option);
+ }
+
+ i.setState({ searchText: "" });
+ }
+}
+
export class SearchableSelect extends Component<
SearchableSelectProps,
SearchableSelectState
> {
- private searchInputRef: RefObject = createRef();
- private toggleButtonRef: RefObject = createRef();
+ searchInputRef: RefObject = createRef();
+ toggleButtonRef: RefObject = createRef();
private loadingEllipsesInterval?: NodeJS.Timer = undefined;
state: SearchableSelectState = {
@@ -55,9 +81,6 @@ export class SearchableSelect extends Component<
constructor(props: SearchableSelectProps, context: any) {
super(props, context);
- this.handleChange = this.handleChange.bind(this);
- this.focusSearch = this.focusSearch.bind(this);
-
if (props.value) {
let selectedIndex = props.options.findIndex(
({ value }) => value === props.value?.toString()
@@ -86,7 +109,8 @@ export class SearchableSelect extends Component<
className="custom-select text-start"
aria-haspopup="listbox"
data-bs-toggle="dropdown"
- onClick={this.focusSearch}
+ onClick={linkEvent(this, focusSearch)}
+ ref={this.toggleButtonRef}
>
{loading
? `${i18n.t("loading")}${loadingEllipses}`
@@ -127,7 +151,7 @@ export class SearchableSelect extends Component<
aria-disabled={option.disabled}
disabled={option.disabled}
aria-selected={selectedIndex === index}
- onClick={() => this.handleChange(option)}
+ onClick={linkEvent({ i: this, option }, handleChange)}
type="button"
>
{option.label}
@@ -138,20 +162,6 @@ export class SearchableSelect extends Component<
);
}
- focusSearch() {
- if (this.toggleButtonRef.current?.ariaExpanded !== "true") {
- this.searchInputRef.current?.focus();
-
- if (this.props.onSearch) {
- this.props.onSearch("");
- }
-
- this.setState({
- searchText: "",
- });
- }
- }
-
static getDerivedStateFromProps({
value,
options,
@@ -189,16 +199,4 @@ export class SearchableSelect extends Component<
clearInterval(this.loadingEllipsesInterval);
}
}
-
- handleChange(option: Choice) {
- const { onChange, value } = this.props;
-
- if (option.value !== value?.toString()) {
- if (onChange) {
- onChange(option);
- }
-
- this.setState({ searchText: "" });
- }
- }
}
diff --git a/src/shared/components/common/sort-select.tsx b/src/shared/components/common/sort-select.tsx
index f54d87d8..dac6e20d 100644
--- a/src/shared/components/common/sort-select.tsx
+++ b/src/shared/components/common/sort-select.tsx
@@ -6,7 +6,7 @@ import { Icon } from "./icon";
interface SortSelectProps {
sort: SortType;
- onChange?(val: SortType): any;
+ onChange(val: SortType): void;
hideHot?: boolean;
hideMostComments?: boolean;
}
@@ -25,7 +25,7 @@ export class SortSelect extends Component {
super(props, context);
}
- static getDerivedStateFromProps(props: any): SortSelectState {
+ static getDerivedStateFromProps(props: SortSelectProps): SortSelectState {
return {
sort: props.sort,
};
@@ -85,6 +85,6 @@ export class SortSelect extends Component {
}
handleSortChange(i: SortSelect, event: any) {
- i.props.onChange?.(event.target.value);
+ i.props.onChange(event.target.value);
}
}
diff --git a/src/shared/components/community/communities.tsx b/src/shared/components/community/communities.tsx
index 53e0e967..3eb7bd3a 100644
--- a/src/shared/components/community/communities.tsx
+++ b/src/shared/components/community/communities.tsx
@@ -1,33 +1,27 @@
import { Component, linkEvent } from "inferno";
import {
CommunityResponse,
- FollowCommunity,
GetSiteResponse,
ListCommunities,
ListCommunitiesResponse,
ListingType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
-import { InitialFetchRequest } from "shared/interfaces";
import { i18n } from "../../i18next";
-import { WebSocketService } from "../../services";
+import { InitialFetchRequest } from "../../interfaces";
+import { FirstLoadService } from "../../services/FirstLoadService";
+import { HttpService, RequestState } from "../../services/HttpService";
import {
QueryParams,
- WithPromiseKeys,
+ RouteDataResponse,
+ editCommunity,
getPageFromString,
getQueryParams,
getQueryString,
- isBrowser,
myAuth,
+ myAuthRequired,
numToSI,
setIsoData,
showLocal,
- toast,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
@@ -37,15 +31,15 @@ import { CommunityLink } from "./community-link";
const communityLimit = 50;
-interface CommunitiesData {
+type CommunitiesData = RouteDataResponse<{
listCommunitiesResponse: ListCommunitiesResponse;
-}
+}>;
interface CommunitiesState {
- listCommunitiesResponse?: ListCommunitiesResponse;
- loading: boolean;
+ listCommunitiesResponse: RequestState;
siteRes: GetSiteResponse;
searchText: string;
+ isIsomorphic: boolean;
}
interface CommunitiesProps {
@@ -53,51 +47,17 @@ interface CommunitiesProps {
page: number;
}
-function getCommunitiesQueryParams() {
- return getQueryParams({
- listingType: getListingTypeFromQuery,
- page: getPageFromString,
- });
-}
-
function getListingTypeFromQuery(listingType?: string): ListingType {
return listingType ? (listingType as ListingType) : "Local";
}
-function toggleSubscribe(community_id: number, follow: boolean) {
- const auth = myAuth();
- if (auth) {
- const form: FollowCommunity = {
- community_id,
- follow,
- auth,
- };
-
- WebSocketService.Instance.send(wsClient.followCommunity(form));
- }
-}
-
-function refetch() {
- const { listingType, page } = getCommunitiesQueryParams();
-
- const listCommunitiesForm: ListCommunities = {
- type_: listingType,
- sort: "TopMonth",
- limit: communityLimit,
- page,
- auth: myAuth(false),
- };
-
- WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
-}
-
export class Communities extends Component {
- private subscription?: Subscription;
private isoData = setIsoData(this.context);
state: CommunitiesState = {
- loading: true,
+ listCommunitiesResponse: { state: "empty" },
siteRes: this.isoData.site_res,
searchText: "",
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
@@ -105,26 +65,21 @@ export class Communities extends Component {
this.handlePageChange = this.handlePageChange.bind(this);
this.handleListingTypeChange = this.handleListingTypeChange.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) {
+ if (FirstLoadService.isFirstLoad) {
const { listCommunitiesResponse } = this.isoData.routeData;
this.state = {
...this.state,
listCommunitiesResponse,
- loading: false,
+ isIsomorphic: true,
};
- } else {
- refetch();
}
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.refetch();
}
}
@@ -134,20 +89,17 @@ export class Communities extends Component {
}`;
}
- render() {
- const { listingType, page } = getCommunitiesQueryParams();
-
- return (
-
-
- {this.state.loading ? (
+ renderListings() {
+ switch (this.state.listCommunitiesResponse.state) {
+ case "loading":
+ return (
- ) : (
+ );
+ case "success": {
+ const { listingType, page } = this.getCommunitiesQueryParams();
+ return (
@@ -188,60 +140,82 @@ export class Communities extends Component
{
- {this.state.listCommunitiesResponse?.communities.map(cv => (
-
-
-
-
-
- {numToSI(cv.counts.subscribers)}
-
-
- {numToSI(cv.counts.users_active_month)}
-
-
- {numToSI(cv.counts.posts)}
-
-
- {numToSI(cv.counts.comments)}
-
-
- {cv.subscribed == "Subscribed" && (
-
- {i18n.t("unsubscribe")}
-
- )}
- {cv.subscribed === "NotSubscribed" && (
-
- {i18n.t("subscribe")}
-
- )}
- {cv.subscribed === "Pending" && (
-
- {i18n.t("subscribe_pending")}
-
- )}
-
-
- ))}
+ {this.state.listCommunitiesResponse.data.communities.map(
+ cv => (
+
+
+
+
+
+ {numToSI(cv.counts.subscribers)}
+
+
+ {numToSI(cv.counts.users_active_month)}
+
+
+ {numToSI(cv.counts.posts)}
+
+
+ {numToSI(cv.counts.comments)}
+
+
+ {cv.subscribed == "Subscribed" && (
+
+ {i18n.t("unsubscribe")}
+
+ )}
+ {cv.subscribed === "NotSubscribed" && (
+
+ {i18n.t("subscribe")}
+
+ )}
+ {cv.subscribed === "Pending" && (
+
+ {i18n.t("subscribe_pending")}
+
+ )}
+
+
+ )
+ )}
- )}
+ );
+ }
+ }
+ }
+
+ render() {
+ return (
+
+
+ {this.renderListings()}
);
}
@@ -272,9 +246,9 @@ export class Communities extends Component
{
);
}
- updateUrl({ listingType, page }: Partial) {
+ async updateUrl({ listingType, page }: Partial) {
const { listingType: urlListingType, page: urlPage } =
- getCommunitiesQueryParams();
+ this.getCommunitiesQueryParams();
const queryParams: QueryParams = {
listingType: listingType ?? urlListingType,
@@ -283,7 +257,7 @@ export class Communities extends Component {
this.props.history.push(`/communities${getQueryString(queryParams)}`);
- refetch();
+ await this.refetch();
}
handlePageChange(page: number) {
@@ -297,30 +271,23 @@ export class Communities extends Component {
});
}
- handleUnsubscribe(communityId: number) {
- toggleSubscribe(communityId, false);
- }
-
- handleSubscribe(communityId: number) {
- toggleSubscribe(communityId, true);
- }
-
handleSearchChange(i: Communities, event: any) {
i.setState({ searchText: event.target.value });
}
- handleSearchSubmit(i: Communities) {
+ handleSearchSubmit(i: Communities, event: any) {
+ event.preventDefault();
const searchParamEncoded = encodeURIComponent(i.state.searchText);
i.context.router.history.push(`/search?q=${searchParamEncoded}`);
}
- static fetchInitialData({
+ static async fetchInitialData({
query: { listingType, page },
client,
auth,
}: InitialFetchRequest<
QueryParams
- >): WithPromiseKeys {
+ >): Promise {
const listCommunitiesForm: ListCommunities = {
type_: getListingTypeFromQuery(listingType),
sort: "TopMonth",
@@ -330,37 +297,62 @@ export class Communities extends Component {
};
return {
- listCommunitiesResponse: client.listCommunities(listCommunitiesForm),
+ listCommunitiesResponse: await client.listCommunities(
+ listCommunitiesForm
+ ),
};
}
- parseMessage(msg: any) {
- const op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- } else if (op === UserOperation.ListCommunities) {
- const data = wsJsonToRes(msg);
- this.setState({ listCommunitiesResponse: data, loading: false });
- window.scrollTo(0, 0);
- } else if (op === UserOperation.FollowCommunity) {
- const {
- community_view: {
- community,
- subscribed,
- counts: { subscribers },
- },
- } = wsJsonToRes(msg);
- const res = this.state.listCommunitiesResponse;
- const found = res?.communities.find(
- ({ community: { id } }) => id == community.id
- );
+ getCommunitiesQueryParams() {
+ return getQueryParams({
+ listingType: getListingTypeFromQuery,
+ page: getPageFromString,
+ });
+ }
- if (found) {
- found.subscribed = subscribed;
- found.counts.subscribers = subscribers;
- this.setState(this.state);
+ async handleFollow(data: {
+ i: Communities;
+ communityId: number;
+ follow: boolean;
+ }) {
+ const res = await HttpService.client.followCommunity({
+ community_id: data.communityId,
+ follow: data.follow,
+ auth: myAuthRequired(),
+ });
+ data.i.findAndUpdateCommunity(res);
+ }
+
+ async refetch() {
+ this.setState({ listCommunitiesResponse: { state: "loading" } });
+
+ const { listingType, page } = this.getCommunitiesQueryParams();
+
+ this.setState({
+ listCommunitiesResponse: await HttpService.client.listCommunities({
+ type_: listingType,
+ sort: "TopMonth",
+ limit: communityLimit,
+ page,
+ auth: myAuth(),
+ }),
+ });
+
+ window.scrollTo(0, 0);
+ }
+
+ findAndUpdateCommunity(res: RequestState) {
+ this.setState(s => {
+ if (
+ s.listCommunitiesResponse.state == "success" &&
+ res.state == "success"
+ ) {
+ s.listCommunitiesResponse.data.communities = editCommunity(
+ res.data.community_view,
+ s.listCommunitiesResponse.data.communities
+ );
}
- }
+ return s;
+ });
}
}
diff --git a/src/shared/components/community/community-form.tsx b/src/shared/components/community/community-form.tsx
index c6ba5d85..4eed4645 100644
--- a/src/shared/components/community/community-form.tsx
+++ b/src/shared/components/community/community-form.tsx
@@ -1,29 +1,17 @@
import { Component, linkEvent } from "inferno";
-import { Prompt } from "inferno-router";
import {
- CommunityResponse,
CommunityView,
CreateCommunity,
EditCommunity,
Language,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
-import { UserService, WebSocketService } from "../../services";
-import {
- capitalizeFirstLetter,
- myAuth,
- randomStr,
- wsClient,
- wsSubscribe,
-} from "../../utils";
+import { capitalizeFirstLetter, myAuthRequired, randomStr } from "../../utils";
import { Icon, Spinner } from "../common/icon";
import { ImageUploadForm } from "../common/image-upload-form";
import { LanguageSelect } from "../common/language-select";
import { MarkdownTextArea } from "../common/markdown-textarea";
+import NavigationPrompt from "../common/navigation-prompt";
interface CommunityFormProps {
community_view?: CommunityView; // If a community is given, that means this is an edit
@@ -31,9 +19,9 @@ interface CommunityFormProps {
siteLanguages: number[];
communityLanguages?: number[];
onCancel?(): any;
- onCreate?(community: CommunityView): any;
- onEdit?(community: CommunityView): any;
+ onUpsertCommunity(form: CreateCommunity | EditCommunity): void;
enableNsfw?: boolean;
+ loading?: boolean;
}
interface CommunityFormState {
@@ -47,7 +35,7 @@ interface CommunityFormState {
posting_restricted_to_mods?: boolean;
discussion_languages?: number[];
};
- loading: boolean;
+ submitted: boolean;
}
export class CommunityForm extends Component<
@@ -55,11 +43,10 @@ export class CommunityForm extends Component<
CommunityFormState
> {
private id = `community-form-${randomStr()}`;
- private subscription?: Subscription;
state: CommunityFormState = {
form: {},
- loading: false,
+ submitted: false,
};
constructor(props: any, context: any) {
@@ -77,12 +64,11 @@ export class CommunityForm extends Component<
this.handleDiscussionLanguageChange =
this.handleDiscussionLanguageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
- let cv = this.props.community_view;
+ const cv = this.props.community_view;
if (cv) {
this.state = {
+ ...this.state,
form: {
name: cv.community.name,
title: cv.community.title,
@@ -93,80 +79,34 @@ export class CommunityForm extends Component<
posting_restricted_to_mods: cv.community.posting_restricted_to_mods,
discussion_languages: this.props.communityLanguages,
},
- loading: false,
};
}
}
- componentDidUpdate() {
- if (
- !this.state.loading &&
- (this.state.form.name ||
- this.state.form.title ||
- this.state.form.description)
- ) {
- window.onbeforeunload = () => true;
- } else {
- window.onbeforeunload = null;
- }
- }
-
- componentWillUnmount() {
- this.subscription?.unsubscribe();
- window.onbeforeunload = null;
- }
-
render() {
return (
- <>
-
+
-
- {!this.props.community_view && (
-
-
- {i18n.t("name")}
-
-
-
-
-
-
-
-
- )}
+ {!this.props.community_view && (
- {i18n.t("display_name")}
+ {i18n.t("name")}
@@ -174,142 +114,182 @@ export class CommunityForm extends Component<
-
-
{i18n.t("icon")}
-
-
-
+ )}
+
+
+ {i18n.t("display_name")}
+
+
+
+
+
+
-
-
{i18n.t("banner")}
-
-
-
+
+
+
{i18n.t("icon")}
+
+
-
-
- {i18n.t("sidebar")}
-
-
-
-
+
+
+
{i18n.t("banner")}
+
+
+
+
+
+ {i18n.t("sidebar")}
+
+
+
+
+
- {this.props.enableNsfw && (
-
-
- {i18n.t("nsfw")}
-
-
-
- )}
+ {this.props.enableNsfw && (
-
- {i18n.t("only_mods_can_post_in_community")}
+
+ {i18n.t("nsfw")}
-
-
-
-
-
- {this.state.loading ? (
-
- ) : this.props.community_view ? (
- capitalizeFirstLetter(i18n.t("save"))
- ) : (
- capitalizeFirstLetter(i18n.t("create"))
+ )}
+
+
+ {i18n.t("only_mods_can_post_in_community")}
+
+
+
+
- {this.props.community_view && (
-
- {i18n.t("cancel")}
-
- )}
+ />
-
- >
+
+
+
+
+
+ {this.props.loading ? (
+
+ ) : this.props.community_view ? (
+ capitalizeFirstLetter(i18n.t("save"))
+ ) : (
+ capitalizeFirstLetter(i18n.t("create"))
+ )}
+
+ {this.props.community_view && (
+
+ {i18n.t("cancel")}
+
+ )}
+
+
+
);
}
handleCreateCommunitySubmit(i: CommunityForm, event: any) {
event.preventDefault();
- i.setState({ loading: true });
- let cForm = i.state.form;
- let auth = myAuth();
+ i.setState({ submitted: true });
+ const cForm = i.state.form;
+ const auth = myAuthRequired();
- let cv = i.props.community_view;
+ const cv = i.props.community_view;
- if (auth) {
- if (cv) {
- let form: EditCommunity = {
- community_id: cv.community.id,
+ if (cv) {
+ i.props.onUpsertCommunity({
+ community_id: cv.community.id,
+ title: cForm.title,
+ description: cForm.description,
+ icon: cForm.icon,
+ banner: cForm.banner,
+ nsfw: cForm.nsfw,
+ posting_restricted_to_mods: cForm.posting_restricted_to_mods,
+ discussion_languages: cForm.discussion_languages,
+ auth,
+ });
+ } else {
+ if (cForm.title && cForm.name) {
+ i.props.onUpsertCommunity({
+ name: cForm.name,
title: cForm.title,
description: cForm.description,
icon: cForm.icon,
@@ -318,37 +298,17 @@ export class CommunityForm extends Component<
posting_restricted_to_mods: cForm.posting_restricted_to_mods,
discussion_languages: cForm.discussion_languages,
auth,
- };
-
- WebSocketService.Instance.send(wsClient.editCommunity(form));
- } else {
- if (cForm.title && cForm.name) {
- let form: CreateCommunity = {
- name: cForm.name,
- title: cForm.title,
- description: cForm.description,
- icon: cForm.icon,
- banner: cForm.banner,
- nsfw: cForm.nsfw,
- posting_restricted_to_mods: cForm.posting_restricted_to_mods,
- discussion_languages: cForm.discussion_languages,
- auth,
- };
- WebSocketService.Instance.send(wsClient.createCommunity(form));
- }
+ });
}
}
- i.setState(i.state);
}
handleCommunityNameChange(i: CommunityForm, event: any) {
- i.state.form.name = event.target.value;
- i.setState(i.state);
+ i.setState(s => ((s.form.name = event.target.value), s));
}
handleCommunityTitleChange(i: CommunityForm, event: any) {
- i.state.form.title = event.target.value;
- i.setState(i.state);
+ i.setState(s => ((s.form.title = event.target.value), s));
}
handleCommunityDescriptionChange(val: string) {
@@ -356,13 +316,13 @@ export class CommunityForm extends Component<
}
handleCommunityNsfwChange(i: CommunityForm, event: any) {
- i.state.form.nsfw = event.target.checked;
- i.setState(i.state);
+ i.setState(s => ((s.form.nsfw = event.target.checked), s));
}
handleCommunityPostingRestrictedToMods(i: CommunityForm, event: any) {
- i.state.form.posting_restricted_to_mods = event.target.checked;
- i.setState(i.state);
+ i.setState(
+ s => ((s.form.posting_restricted_to_mods = event.target.checked), s)
+ );
}
handleCancel(i: CommunityForm) {
@@ -388,56 +348,4 @@ export class CommunityForm extends Component<
handleDiscussionLanguageChange(val: number[]) {
this.setState(s => ((s.form.discussion_languages = val), s));
}
-
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- // Errors handled by top level pages
- // toast(i18n.t(msg.error), "danger");
- this.setState({ loading: false });
- return;
- } else if (op == UserOperation.CreateCommunity) {
- let data = wsJsonToRes(msg);
- this.props.onCreate?.(data.community_view);
-
- // Update myUserInfo
- let community = data.community_view.community;
-
- let mui = UserService.Instance.myUserInfo;
- if (mui) {
- let person = mui.local_user_view.person;
- mui.follows.push({
- community,
- follower: person,
- });
- mui.moderates.push({
- community,
- moderator: person,
- });
- }
- } else if (op == UserOperation.EditCommunity) {
- let data = wsJsonToRes(msg);
- this.setState({ loading: false });
- this.props.onEdit?.(data.community_view);
- let community = data.community_view.community;
-
- let mui = UserService.Instance.myUserInfo;
- if (mui) {
- let followFound = mui.follows.findIndex(
- f => f.community.id == community.id
- );
- if (followFound) {
- mui.follows[followFound].community = community;
- }
-
- let moderatesFound = mui.moderates.findIndex(
- f => f.community.id == community.id
- );
- if (moderatesFound) {
- mui.moderates[moderatesFound].community = community;
- }
- }
- }
- }
}
diff --git a/src/shared/components/community/community-link.tsx b/src/shared/components/community/community-link.tsx
index bf15971c..23d52ae6 100644
--- a/src/shared/components/community/community-link.tsx
+++ b/src/shared/components/community/community-link.tsx
@@ -18,22 +18,22 @@ export class CommunityLink extends Component {
}
render() {
- let community = this.props.community;
+ const community = this.props.community;
let name_: string, title: string, link: string;
- let local = community.local == null ? true : community.local;
+ const local = community.local == null ? true : community.local;
if (local) {
name_ = community.name;
title = community.title;
link = `/c/${community.name}`;
} else {
- let domain = hostname(community.actor_id);
+ const domain = hostname(community.actor_id);
name_ = `${community.name}@${domain}`;
title = `${community.title}@${domain}`;
link = !this.props.realLink ? `/c/${name_}` : community.actor_id;
}
- let apubName = `!${name_}`;
- let displayName = this.props.useApubName ? apubName : title;
+ const apubName = `!${name_}`;
+ const displayName = this.props.useApubName ? apubName : title;
return !this.props.realLink ? (
{
}
avatarAndName(displayName: string) {
- let icon = this.props.community.icon;
+ const icon = this.props.community.icon;
return (
<>
{!this.props.hideAvatar &&
diff --git a/src/shared/components/community/community.tsx b/src/shared/components/community/community.tsx
index f3ab1e21..e8412e76 100644
--- a/src/shared/components/community/community.tsx
+++ b/src/shared/components/community/community.tsx
@@ -1,60 +1,86 @@
import { Component, linkEvent } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import {
+ AddAdmin,
+ AddModToCommunity,
AddModToCommunityResponse,
+ BanFromCommunity,
BanFromCommunityResponse,
- BlockCommunityResponse,
- BlockPersonResponse,
+ BanPerson,
+ BanPersonResponse,
+ BlockCommunity,
+ BlockPerson,
+ CommentId,
+ CommentReplyResponse,
CommentResponse,
- CommentView,
CommunityResponse,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ CreatePostLike,
+ CreatePostReport,
+ DeleteComment,
+ DeleteCommunity,
+ DeletePost,
+ DistinguishComment,
+ EditComment,
+ EditCommunity,
+ EditPost,
+ FeaturePost,
+ FollowCommunity,
GetComments,
GetCommentsResponse,
GetCommunity,
GetCommunityResponse,
GetPosts,
GetPostsResponse,
- PostReportResponse,
+ GetSiteResponse,
+ LockPost,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
PostResponse,
- PostView,
+ PurgeComment,
+ PurgeCommunity,
PurgeItemResponse,
+ PurgePerson,
+ PurgePost,
+ RemoveComment,
+ RemoveCommunity,
+ RemovePost,
+ SaveComment,
+ SavePost,
SortType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ TransferCommunity,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import {
CommentViewType,
DataType,
InitialFetchRequest,
} from "../../interfaces";
-import { UserService, WebSocketService } from "../../services";
+import { UserService } from "../../services";
+import { FirstLoadService } from "../../services/FirstLoadService";
+import { HttpService, RequestState } from "../../services/HttpService";
import {
QueryParams,
- WithPromiseKeys,
+ RouteDataResponse,
commentsToFlatNodes,
communityRSSUrl,
- createCommentLikeRes,
- createPostLikeFindRes,
- editCommentRes,
- editPostFindRes,
+ editComment,
+ editPost,
+ editWith,
enableDownvotes,
enableNsfw,
fetchLimit,
+ getCommentParentId,
getDataTypeString,
getPageFromString,
getQueryParams,
getQueryString,
- isPostBlocked,
myAuth,
- notifyPost,
- nsfwCheck,
postToCommentSortType,
relTags,
restoreScrollPosition,
- saveCommentRes,
saveScrollPosition,
setIsoData,
setupTippy,
@@ -62,8 +88,6 @@ import {
toast,
updateCommunityBlock,
updatePersonBlock,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { CommentNodes } from "../comment/comment-nodes";
import { BannerIconHeader } from "../common/banner-icon-header";
@@ -77,19 +101,20 @@ import { SiteSidebar } from "../home/site-sidebar";
import { PostListings } from "../post/post-listings";
import { CommunityLink } from "./community-link";
-interface CommunityData {
+type CommunityData = RouteDataResponse<{
communityResponse: GetCommunityResponse;
postsResponse?: GetPostsResponse;
commentsResponse?: GetCommentsResponse;
-}
+}>;
interface State {
- communityRes?: GetCommunityResponse;
- communityLoading: boolean;
- listingsLoading: boolean;
- posts: PostView[];
- comments: CommentView[];
+ communityRes: RequestState;
+ postsRes: RequestState;
+ commentsRes: RequestState;
+ siteRes: GetSiteResponse;
showSidebarMobile: boolean;
+ finished: Map;
+ isIsomorphic: boolean;
}
interface CommunityProps {
@@ -123,13 +148,14 @@ export class Community extends Component<
State
> {
private isoData = setIsoData(this.context);
- private subscription?: Subscription;
state: State = {
- communityLoading: true,
- listingsLoading: true,
- posts: [],
- comments: [],
+ communityRes: { state: "empty" },
+ postsRes: { state: "empty" },
+ commentsRes: { state: "empty" },
+ siteRes: this.isoData.site_res,
showSidebarMobile: false,
+ finished: new Map(),
+ isIsomorphic: false,
};
constructor(props: RouteComponentProps<{ name: string }>, context: any) {
@@ -139,63 +165,106 @@ export class Community extends Component<
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ // All of the action binds
+ this.handleDeleteCommunity = this.handleDeleteCommunity.bind(this);
+ this.handleEditCommunity = this.handleEditCommunity.bind(this);
+ this.handleFollow = this.handleFollow.bind(this);
+ this.handleRemoveCommunity = this.handleRemoveCommunity.bind(this);
+ this.handleCreateComment = this.handleCreateComment.bind(this);
+ this.handleEditComment = this.handleEditComment.bind(this);
+ this.handleSaveComment = this.handleSaveComment.bind(this);
+ this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
+ this.handleBlockPerson = this.handleBlockPerson.bind(this);
+ this.handleDeleteComment = this.handleDeleteComment.bind(this);
+ this.handleRemoveComment = this.handleRemoveComment.bind(this);
+ this.handleCommentVote = this.handleCommentVote.bind(this);
+ this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+ this.handleAddAdmin = this.handleAddAdmin.bind(this);
+ this.handlePurgePerson = this.handlePurgePerson.bind(this);
+ this.handlePurgeComment = this.handlePurgeComment.bind(this);
+ this.handleCommentReport = this.handleCommentReport.bind(this);
+ this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+ this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+ this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+ this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+ this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+ this.handleBanPerson = this.handleBanPerson.bind(this);
+ this.handlePostVote = this.handlePostVote.bind(this);
+ this.handlePostEdit = this.handlePostEdit.bind(this);
+ this.handlePostReport = this.handlePostReport.bind(this);
+ this.handleLockPost = this.handleLockPost.bind(this);
+ this.handleDeletePost = this.handleDeletePost.bind(this);
+ this.handleRemovePost = this.handleRemovePost.bind(this);
+ this.handleSavePost = this.handleSavePost.bind(this);
+ this.handlePurgePost = this.handlePurgePost.bind(this);
+ this.handleFeaturePost = this.handleFeaturePost.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- const { communityResponse, commentsResponse, postsResponse } =
- this.isoData.routeData;
+ if (FirstLoadService.isFirstLoad) {
+ const {
+ communityResponse: communityRes,
+ commentsResponse: commentsRes,
+ postsResponse: postsRes,
+ } = this.isoData.routeData;
this.state = {
...this.state,
- communityRes: communityResponse,
+ isIsomorphic: true,
};
- if (postsResponse) {
- this.state = { ...this.state, posts: postsResponse.posts };
+ if (communityRes.state === "success") {
+ this.state = {
+ ...this.state,
+ communityRes,
+ };
}
- if (commentsResponse) {
- this.state = { ...this.state, comments: commentsResponse.comments };
+ if (postsRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ postsRes,
+ };
}
- this.state = {
- ...this.state,
- communityLoading: false,
- listingsLoading: false,
- };
- } else {
- this.fetchCommunity();
- this.fetchData();
+ if (commentsRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ commentsRes,
+ };
+ }
}
}
- fetchCommunity() {
- const form: GetCommunity = {
- name: this.props.match.params.name,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(wsClient.getCommunity(form));
+ async fetchCommunity() {
+ this.setState({ communityRes: { state: "loading" } });
+ this.setState({
+ communityRes: await HttpService.client.getCommunity({
+ name: this.props.match.params.name,
+ auth: myAuth(),
+ }),
+ });
}
- componentDidMount() {
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await Promise.all([this.fetchCommunity(), this.fetchData()]);
+ }
+
setupTippy();
}
componentWillUnmount() {
saveScrollPosition(this.context);
- this.subscription?.unsubscribe();
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
path,
query: { dataType: urlDataType, page: urlPage, sort: urlSort },
auth,
- }: InitialFetchRequest<
- QueryParams
- >): WithPromiseKeys {
+ }: InitialFetchRequest>): Promise<
+ Promise
+ > {
const pathSplit = path.split("/");
const communityName = pathSplit[2];
@@ -210,8 +279,9 @@ export class Community extends Component<
const page = getPageFromString(urlPage);
- let postsResponse: Promise | undefined = undefined;
- let commentsResponse: Promise | undefined = undefined;
+ let postsResponse: RequestState | undefined = undefined;
+ let commentsResponse: RequestState | undefined =
+ undefined;
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
@@ -224,7 +294,7 @@ export class Community extends Component<
auth,
};
- postsResponse = client.getPosts(getPostsForm);
+ postsResponse = await client.getPosts(getPostsForm);
} else {
const getCommentsForm: GetComments = {
community_name: communityName,
@@ -236,11 +306,11 @@ export class Community extends Component<
auth,
};
- commentsResponse = client.getComments(getCommentsForm);
+ commentsResponse = await client.getComments(getCommentsForm);
}
return {
- communityResponse: client.getCommunity(communityForm),
+ communityResponse: await client.getCommunity(communityForm),
commentsResponse,
postsResponse,
};
@@ -248,141 +318,192 @@ export class Community extends Component<
get documentTitle(): string {
const cRes = this.state.communityRes;
- return cRes
- ? `${cRes.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
+ return cRes.state == "success"
+ ? `${cRes.data.community_view.community.title} - ${this.isoData.site_res.site_view.site.name}`
: "";
}
- render() {
- const res = this.state.communityRes;
- const { page } = getCommunityQueryParams();
-
- return (
-
- {this.state.communityLoading ? (
+ renderCommunity() {
+ switch (this.state.communityRes.state) {
+ case "loading":
+ return (
- ) : (
- res && (
- <>
-
+ );
+ case "success": {
+ const res = this.state.communityRes.data;
+ const { page } = getCommunityQueryParams();
-
-
- {this.communityInfo}
-
-
- {i18n.t("sidebar")}{" "}
-
-
- {this.state.showSidebarMobile && this.sidebar(res)}
-
- {this.selects}
- {this.listings}
-
-
-
- {this.sidebar(res)}
+ return (
+ <>
+
+
+
+
+ {this.communityInfo(res)}
+
+
+ {i18n.t("sidebar")}{" "}
+
+
+ {this.state.showSidebarMobile && this.sidebar(res)}
+ {this.selects(res)}
+ {this.listings(res)}
+
- >
- )
- )}
-
- );
+
+ {this.sidebar(res)}
+
+
+ >
+ );
+ }
+ }
}
- sidebar({
- community_view,
- moderators,
- online,
- discussion_languages,
- site,
- }: GetCommunityResponse) {
+ render() {
+ return
{this.renderCommunity()}
;
+ }
+
+ sidebar(res: GetCommunityResponse) {
const { site_res } = this.isoData;
// For some reason, this returns an empty vec if it matches the site langs
const communityLangs =
- discussion_languages.length === 0
+ res.discussion_languages.length === 0
? site_res.all_languages.map(({ id }) => id)
- : discussion_languages;
+ : res.discussion_languages;
return (
<>
- {!community_view.community.local && site && (
-
+ {!res.community_view.community.local && res.site && (
+
)}
>
);
}
- get listings() {
+ listings(communityRes: GetCommunityResponse) {
const { dataType } = getCommunityQueryParams();
const { site_res } = this.isoData;
- const { listingsLoading, posts, comments, communityRes } = this.state;
- if (listingsLoading) {
- return (
-
-
-
- );
- } else if (dataType === DataType.Post) {
- return (
-
- );
+ if (dataType === DataType.Post) {
+ switch (this.state.postsRes.state) {
+ case "loading":
+ return (
+
+
+
+ );
+ case "success":
+ return (
+
+ );
+ }
} else {
- return (
-
- );
+ switch (this.state.commentsRes.state) {
+ case "loading":
+ return (
+
+
+
+ );
+ case "success":
+ return (
+
+ );
+ }
}
}
- get communityInfo() {
- const community = this.state.communityRes?.community_view.community;
+ communityInfo(res: GetCommunityResponse) {
+ const community = res.community_view.community;
return (
community && (
@@ -401,12 +522,11 @@ export class Community extends Component<
);
}
- get selects() {
+ selects(res: GetCommunityResponse) {
// let communityRss = this.state.communityRes.map(r =>
// communityRSSUrl(r.community_view.community.actor_id, this.state.sort)
// );
const { dataType, sort } = getCommunityQueryParams();
- const res = this.state.communityRes;
const communityRss = res
? communityRSSUrl(res.community_view.community.actor_id, sort)
: undefined;
@@ -459,7 +579,7 @@ export class Community extends Component<
}));
}
- updateUrl({ dataType, page, sort }: Partial
) {
+ async updateUrl({ dataType, page, sort }: Partial) {
const {
dataType: urlDataType,
page: urlPage,
@@ -476,284 +596,368 @@ export class Community extends Component<
`/c/${this.props.match.params.name}${getQueryString(queryParams)}`
);
- this.setState({
- comments: [],
- posts: [],
- listingsLoading: true,
- });
-
- this.fetchData();
+ await this.fetchData();
}
- fetchData() {
+ async fetchData() {
const { dataType, page, sort } = getCommunityQueryParams();
const { name } = this.props.match.params;
- let req: string;
if (dataType === DataType.Post) {
- const form: GetPosts = {
- page,
- limit: fetchLimit,
- sort,
- type_: "All",
- community_name: name,
- saved_only: false,
- auth: myAuth(false),
- };
- req = wsClient.getPosts(form);
+ this.setState({ postsRes: { state: "loading" } });
+ this.setState({
+ postsRes: await HttpService.client.getPosts({
+ page,
+ limit: fetchLimit,
+ sort,
+ type_: "All",
+ community_name: name,
+ saved_only: false,
+ auth: myAuth(),
+ }),
+ });
} else {
- const form: GetComments = {
- page,
- limit: fetchLimit,
- sort: postToCommentSortType(sort),
- type_: "All",
- community_name: name,
- saved_only: false,
- auth: myAuth(false),
- };
-
- req = wsClient.getComments(form);
+ this.setState({ commentsRes: { state: "loading" } });
+ this.setState({
+ commentsRes: await HttpService.client.getComments({
+ page,
+ limit: fetchLimit,
+ sort: postToCommentSortType(sort),
+ type_: "All",
+ community_name: name,
+ saved_only: false,
+ auth: myAuth(),
+ }),
+ });
}
- WebSocketService.Instance.send(req);
+ restoreScrollPosition(this.context);
+ setupTippy();
}
- parseMessage(msg: any) {
- const { page } = getCommunityQueryParams();
- const op = wsUserOp(msg);
- console.log(msg);
- const res = this.state.communityRes;
+ async handleDeleteCommunity(form: DeleteCommunity) {
+ const deleteCommunityRes = await HttpService.client.deleteCommunity(form);
+ this.updateCommunity(deleteCommunityRes);
+ }
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.context.router.history.push("/");
- } else if (msg.reconnect) {
- if (res) {
- WebSocketService.Instance.send(
- wsClient.communityJoin({
- community_id: res.community_view.community.id,
- })
+ async handleAddModToCommunity(form: AddModToCommunity) {
+ const addModRes = await HttpService.client.addModToCommunity(form);
+ this.updateModerators(addModRes);
+ }
+
+ async handleFollow(form: FollowCommunity) {
+ const followCommunityRes = await HttpService.client.followCommunity(form);
+ this.updateCommunity(followCommunityRes);
+
+ // Update myUserInfo
+ if (followCommunityRes.state == "success") {
+ const communityId = followCommunityRes.data.community_view.community.id;
+ const mui = UserService.Instance.myUserInfo;
+ if (mui) {
+ mui.follows = mui.follows.filter(i => i.community.id != communityId);
+ }
+ }
+ }
+
+ async handlePurgeCommunity(form: PurgeCommunity) {
+ const purgeCommunityRes = await HttpService.client.purgeCommunity(form);
+ this.purgeItem(purgeCommunityRes);
+ }
+
+ async handlePurgePerson(form: PurgePerson) {
+ const purgePersonRes = await HttpService.client.purgePerson(form);
+ this.purgeItem(purgePersonRes);
+ }
+
+ async handlePurgeComment(form: PurgeComment) {
+ const purgeCommentRes = await HttpService.client.purgeComment(form);
+ this.purgeItem(purgeCommentRes);
+ }
+
+ async handlePurgePost(form: PurgePost) {
+ const purgeRes = await HttpService.client.purgePost(form);
+ this.purgeItem(purgeRes);
+ }
+
+ async handleBlockCommunity(form: BlockCommunity) {
+ const blockCommunityRes = await HttpService.client.blockCommunity(form);
+ if (blockCommunityRes.state == "success") {
+ updateCommunityBlock(blockCommunityRes.data);
+ }
+ }
+
+ async handleBlockPerson(form: BlockPerson) {
+ const blockPersonRes = await HttpService.client.blockPerson(form);
+ if (blockPersonRes.state == "success") {
+ updatePersonBlock(blockPersonRes.data);
+ }
+ }
+
+ async handleRemoveCommunity(form: RemoveCommunity) {
+ const removeCommunityRes = await HttpService.client.removeCommunity(form);
+ this.updateCommunity(removeCommunityRes);
+ }
+
+ async handleEditCommunity(form: EditCommunity) {
+ const res = await HttpService.client.editCommunity(form);
+ this.updateCommunity(res);
+
+ return res;
+ }
+
+ async handleCreateComment(form: CreateComment) {
+ const createCommentRes = await HttpService.client.createComment(form);
+ this.createAndUpdateComments(createCommentRes);
+
+ return createCommentRes;
+ }
+
+ async handleEditComment(form: EditComment) {
+ const editCommentRes = await HttpService.client.editComment(form);
+ this.findAndUpdateComment(editCommentRes);
+
+ return editCommentRes;
+ }
+
+ async handleDeleteComment(form: DeleteComment) {
+ const deleteCommentRes = await HttpService.client.deleteComment(form);
+ this.findAndUpdateComment(deleteCommentRes);
+ }
+
+ async handleDeletePost(form: DeletePost) {
+ const deleteRes = await HttpService.client.deletePost(form);
+ this.findAndUpdatePost(deleteRes);
+ }
+
+ async handleRemovePost(form: RemovePost) {
+ const removeRes = await HttpService.client.removePost(form);
+ this.findAndUpdatePost(removeRes);
+ }
+
+ async handleRemoveComment(form: RemoveComment) {
+ const removeCommentRes = await HttpService.client.removeComment(form);
+ this.findAndUpdateComment(removeCommentRes);
+ }
+
+ async handleSaveComment(form: SaveComment) {
+ const saveCommentRes = await HttpService.client.saveComment(form);
+ this.findAndUpdateComment(saveCommentRes);
+ }
+
+ async handleSavePost(form: SavePost) {
+ const saveRes = await HttpService.client.savePost(form);
+ this.findAndUpdatePost(saveRes);
+ }
+
+ async handleFeaturePost(form: FeaturePost) {
+ const featureRes = await HttpService.client.featurePost(form);
+ this.findAndUpdatePost(featureRes);
+ }
+
+ async handleCommentVote(form: CreateCommentLike) {
+ const voteRes = await HttpService.client.likeComment(form);
+ this.findAndUpdateComment(voteRes);
+ }
+
+ async handlePostEdit(form: EditPost) {
+ const res = await HttpService.client.editPost(form);
+ this.findAndUpdatePost(res);
+ }
+
+ async handlePostVote(form: CreatePostLike) {
+ const voteRes = await HttpService.client.likePost(form);
+ this.findAndUpdatePost(voteRes);
+ }
+
+ async handleCommentReport(form: CreateCommentReport) {
+ const reportRes = await HttpService.client.createCommentReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handlePostReport(form: CreatePostReport) {
+ const reportRes = await HttpService.client.createPostReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handleLockPost(form: LockPost) {
+ const lockRes = await HttpService.client.lockPost(form);
+ this.findAndUpdatePost(lockRes);
+ }
+
+ async handleDistinguishComment(form: DistinguishComment) {
+ const distinguishRes = await HttpService.client.distinguishComment(form);
+ this.findAndUpdateComment(distinguishRes);
+ }
+
+ async handleAddAdmin(form: AddAdmin) {
+ const addAdminRes = await HttpService.client.addAdmin(form);
+
+ if (addAdminRes.state == "success") {
+ this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+ }
+ }
+
+ async handleTransferCommunity(form: TransferCommunity) {
+ const transferCommunityRes = await HttpService.client.transferCommunity(
+ form
+ );
+ toast(i18n.t("transfer_community"));
+ this.updateCommunityFull(transferCommunityRes);
+ }
+
+ async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+ const readRes = await HttpService.client.markCommentReplyAsRead(form);
+ this.findAndUpdateCommentReply(readRes);
+ }
+
+ async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+ // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+ await HttpService.client.markPersonMentionAsRead(form);
+ }
+
+ async handleBanFromCommunity(form: BanFromCommunity) {
+ const banRes = await HttpService.client.banFromCommunity(form);
+ this.updateBanFromCommunity(banRes);
+ }
+
+ async handleBanPerson(form: BanPerson) {
+ const banRes = await HttpService.client.banPerson(form);
+ this.updateBan(banRes);
+ }
+
+ updateBanFromCommunity(banRes: RequestState) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.postsRes.state == "success") {
+ s.postsRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ return s;
+ });
+ }
+ }
+
+ updateBan(banRes: RequestState) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.postsRes.state == "success") {
+ s.postsRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ return s;
+ });
+ }
+ }
+
+ updateCommunity(res: RequestState) {
+ this.setState(s => {
+ if (s.communityRes.state == "success" && res.state == "success") {
+ s.communityRes.data.community_view = res.data.community_view;
+ s.communityRes.data.discussion_languages =
+ res.data.discussion_languages;
+ }
+ return s;
+ });
+ }
+
+ updateCommunityFull(res: RequestState) {
+ this.setState(s => {
+ if (s.communityRes.state == "success" && res.state == "success") {
+ s.communityRes.data.community_view = res.data.community_view;
+ s.communityRes.data.moderators = res.data.moderators;
+ }
+ return s;
+ });
+ }
+
+ purgeItem(purgeRes: RequestState) {
+ if (purgeRes.state == "success") {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
+
+ findAndUpdateComment(res: RequestState) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editComment(
+ res.data.comment_view,
+ s.commentsRes.data.comments
+ );
+ s.finished.set(res.data.comment_view.comment.id, true);
+ }
+ return s;
+ });
+ }
+
+ createAndUpdateComments(res: RequestState) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments.unshift(res.data.comment_view);
+
+ // Set finished for the parent
+ s.finished.set(
+ getCommentParentId(res.data.comment_view.comment) ?? 0,
+ true
);
}
+ return s;
+ });
+ }
- this.fetchData();
- } else {
- switch (op) {
- case UserOperation.GetCommunity: {
- const data = wsJsonToRes(msg);
-
- this.setState({ communityRes: data, communityLoading: false });
- // TODO why is there no auth in this form?
- WebSocketService.Instance.send(
- wsClient.communityJoin({
- community_id: data.community_view.community.id,
- })
- );
-
- break;
- }
-
- case UserOperation.EditCommunity:
- case UserOperation.DeleteCommunity:
- case UserOperation.RemoveCommunity: {
- const { community_view, discussion_languages } =
- wsJsonToRes(msg);
-
- if (res) {
- res.community_view = community_view;
- res.discussion_languages = discussion_languages;
- this.setState(this.state);
- }
-
- break;
- }
-
- case UserOperation.FollowCommunity: {
- const {
- community_view: {
- subscribed,
- counts: { subscribers },
- },
- } = wsJsonToRes(msg);
-
- if (res) {
- res.community_view.subscribed = subscribed;
- res.community_view.counts.subscribers = subscribers;
- this.setState(this.state);
- }
-
- break;
- }
-
- case UserOperation.GetPosts: {
- const { posts } = wsJsonToRes(msg);
-
- this.setState({ posts, listingsLoading: false });
- restoreScrollPosition(this.context);
- setupTippy();
-
- break;
- }
-
- case UserOperation.EditPost:
- case UserOperation.DeletePost:
- case UserOperation.RemovePost:
- case UserOperation.LockPost:
- case UserOperation.FeaturePost:
- case UserOperation.SavePost: {
- const { post_view } = wsJsonToRes(msg);
-
- editPostFindRes(post_view, this.state.posts);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.CreatePost: {
- const { post_view } = wsJsonToRes(msg);
-
- const showPostNotifs =
- UserService.Instance.myUserInfo?.local_user_view.local_user
- .show_new_post_notifs;
-
- // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
- if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
- this.state.posts.unshift(post_view);
- if (showPostNotifs) {
- notifyPost(post_view, this.context.router);
- }
- this.setState(this.state);
- }
-
- break;
- }
-
- case UserOperation.CreatePostLike: {
- const { post_view } = wsJsonToRes(msg);
-
- createPostLikeFindRes(post_view, this.state.posts);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.AddModToCommunity: {
- const { moderators } = wsJsonToRes(msg);
-
- if (res) {
- res.moderators = moderators;
- this.setState(this.state);
- }
-
- break;
- }
-
- case UserOperation.BanFromCommunity: {
- const {
- person_view: {
- person: { id: personId },
- },
- banned,
- } = wsJsonToRes(msg);
-
- // TODO this might be incorrect
- this.state.posts
- .filter(p => p.creator.id === personId)
- .forEach(p => (p.creator_banned_from_community = banned));
-
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.GetComments: {
- const { comments } = wsJsonToRes(msg);
- this.setState({ comments, listingsLoading: false });
-
- break;
- }
-
- case UserOperation.EditComment:
- case UserOperation.DeleteComment:
- case UserOperation.RemoveComment: {
- const { comment_view } = wsJsonToRes(msg);
- editCommentRes(comment_view, this.state.comments);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.CreateComment: {
- const { form_id, comment_view } = wsJsonToRes(msg);
-
- // Necessary since it might be a user reply
- if (form_id) {
- this.setState(({ comments }) => ({
- comments: [comment_view].concat(comments),
- }));
- }
-
- break;
- }
-
- case UserOperation.SaveComment: {
- const { comment_view } = wsJsonToRes(msg);
-
- saveCommentRes(comment_view, this.state.comments);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.CreateCommentLike: {
- const { comment_view } = wsJsonToRes(msg);
-
- createCommentLikeRes(comment_view, this.state.comments);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.BlockPerson: {
- const data = wsJsonToRes(msg);
- updatePersonBlock(data);
-
- break;
- }
-
- case UserOperation.CreatePostReport:
- case UserOperation.CreateCommentReport: {
- const data = wsJsonToRes(msg);
-
- if (data) {
- toast(i18n.t("report_created"));
- }
-
- break;
- }
-
- case UserOperation.PurgeCommunity: {
- const { success } = wsJsonToRes(msg);
-
- if (success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
- }
-
- break;
- }
-
- case UserOperation.BlockCommunity: {
- const data = wsJsonToRes(msg);
- if (res) {
- res.community_view.blocked = data.blocked;
- this.setState(this.state);
- }
- updateCommunityBlock(data);
-
- break;
- }
+ findAndUpdateCommentReply(res: RequestState) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editWith(
+ res.data.comment_reply_view,
+ s.commentsRes.data.comments
+ );
}
- }
+ return s;
+ });
+ }
+
+ findAndUpdatePost(res: RequestState) {
+ this.setState(s => {
+ if (s.postsRes.state == "success" && res.state == "success") {
+ s.postsRes.data.posts = editPost(
+ res.data.post_view,
+ s.postsRes.data.posts
+ );
+ }
+ return s;
+ });
+ }
+
+ updateModerators(res: RequestState) {
+ // Update the moderators
+ this.setState(s => {
+ if (s.communityRes.state == "success" && res.state == "success") {
+ s.communityRes.data.moderators = res.data.moderators;
+ }
+ return s;
+ });
}
}
diff --git a/src/shared/components/community/create-community.tsx b/src/shared/components/community/create-community.tsx
index 36503568..ff31b839 100644
--- a/src/shared/components/community/create-community.tsx
+++ b/src/shared/components/community/create-community.tsx
@@ -1,16 +1,12 @@
import { Component } from "inferno";
-import { CommunityView, GetSiteResponse } from "lemmy-js-client";
-import { Subscription } from "rxjs";
-import { i18n } from "../../i18next";
import {
- enableNsfw,
- isBrowser,
- setIsoData,
- toast,
- wsSubscribe,
-} from "../../utils";
+ CreateCommunity as CreateCommunityI,
+ GetSiteResponse,
+} from "lemmy-js-client";
+import { i18n } from "../../i18next";
+import { HttpService } from "../../services/HttpService";
+import { enableNsfw, setIsoData } from "../../utils";
import { HtmlTags } from "../common/html-tags";
-import { Spinner } from "../common/icon";
import { CommunityForm } from "./community-form";
interface CreateCommunityState {
@@ -20,7 +16,6 @@ interface CreateCommunityState {
export class CreateCommunity extends Component {
private isoData = setIsoData(this.context);
- private subscription?: Subscription;
state: CreateCommunityState = {
siteRes: this.isoData.site_res,
loading: false,
@@ -28,15 +23,6 @@ export class CreateCommunity extends Component {
constructor(props: any, context: any) {
super(props, context);
this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
-
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
- }
-
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
- }
}
get documentTitle(): string {
@@ -52,35 +38,33 @@ export class CreateCommunity extends Component {
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
- {this.state.loading ? (
-
-
-
- ) : (
-
-
-
{i18n.t("create_community")}
-
-
+
+
+
{i18n.t("create_community")}
+
- )}
+
);
}
- handleCommunityCreate(cv: CommunityView) {
- this.props.history.push(`/c/${cv.community.name}`);
- }
+ async handleCommunityCreate(form: CreateCommunityI) {
+ this.setState({ loading: true });
- parseMessage(msg: any) {
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
+ const res = await HttpService.client.createCommunity(form);
+
+ if (res.state === "success") {
+ const name = res.data.community_view.community.name;
+ this.props.history.replace(`/c/${name}`);
+ } else {
+ this.setState({ loading: false });
}
}
}
diff --git a/src/shared/components/community/sidebar.tsx b/src/shared/components/community/sidebar.tsx
index a5eef75c..a5c620f3 100644
--- a/src/shared/components/community/sidebar.tsx
+++ b/src/shared/components/community/sidebar.tsx
@@ -1,4 +1,4 @@
-import { Component, linkEvent } from "inferno";
+import { Component, InfernoNode, linkEvent } from "inferno";
import { Link } from "inferno-router";
import {
AddModToCommunity,
@@ -6,6 +6,7 @@ import {
CommunityModeratorView,
CommunityView,
DeleteCommunity,
+ EditCommunity,
FollowCommunity,
Language,
PersonView,
@@ -13,7 +14,7 @@ import {
RemoveCommunity,
} from "lemmy-js-client";
import { i18n } from "../../i18next";
-import { UserService, WebSocketService } from "../../services";
+import { UserService } from "../../services";
import {
amAdmin,
amMod,
@@ -21,9 +22,8 @@ import {
getUnixTime,
hostname,
mdToHtml,
- myAuth,
+ myAuthRequired,
numToSI,
- wsClient,
} from "../../utils";
import { BannerIconHeader } from "../common/banner-icon-header";
import { Icon, PurgeWarning, Spinner } from "../common/icon";
@@ -42,6 +42,13 @@ interface SidebarProps {
enableNsfw?: boolean;
showIcon?: boolean;
editable?: boolean;
+ onDeleteCommunity(form: DeleteCommunity): void;
+ onRemoveCommunity(form: RemoveCommunity): void;
+ onLeaveModTeam(form: AddModToCommunity): void;
+ onFollowCommunity(form: FollowCommunity): void;
+ onBlockCommunity(form: BlockCommunity): void;
+ onPurgeCommunity(form: PurgeCommunity): void;
+ onEditCommunity(form: EditCommunity): void;
}
interface SidebarState {
@@ -51,8 +58,13 @@ interface SidebarState {
showRemoveDialog: boolean;
showPurgeDialog: boolean;
purgeReason?: string;
- purgeLoading: boolean;
showConfirmLeaveModTeam: boolean;
+ deleteCommunityLoading: boolean;
+ removeCommunityLoading: boolean;
+ leaveModTeamLoading: boolean;
+ followCommunityLoading: boolean;
+ blockCommunityLoading: boolean;
+ purgeCommunityLoading: boolean;
}
export class Sidebar extends Component {
@@ -60,16 +72,44 @@ export class Sidebar extends Component {
showEdit: false,
showRemoveDialog: false,
showPurgeDialog: false,
- purgeLoading: false,
showConfirmLeaveModTeam: false,
+ deleteCommunityLoading: false,
+ removeCommunityLoading: false,
+ leaveModTeamLoading: false,
+ followCommunityLoading: false,
+ blockCommunityLoading: false,
+ purgeCommunityLoading: false,
};
constructor(props: any, context: any) {
super(props, context);
- this.handleEditCommunity = this.handleEditCommunity.bind(this);
this.handleEditCancel = this.handleEditCancel.bind(this);
}
+ componentWillReceiveProps(
+ nextProps: Readonly<{ children?: InfernoNode } & SidebarProps>
+ ): void {
+ if (this.props.moderators != nextProps.moderators) {
+ this.setState({
+ showConfirmLeaveModTeam: false,
+ });
+ }
+
+ if (this.props.community_view != nextProps.community_view) {
+ this.setState({
+ showEdit: false,
+ showPurgeDialog: false,
+ showRemoveDialog: false,
+ deleteCommunityLoading: false,
+ removeCommunityLoading: false,
+ leaveModTeamLoading: false,
+ followCommunityLoading: false,
+ blockCommunityLoading: false,
+ purgeCommunityLoading: false,
+ });
+ }
+ }
+
render() {
return (
@@ -81,7 +121,7 @@ export class Sidebar extends Component
{
allLanguages={this.props.allLanguages}
siteLanguages={this.props.siteLanguages}
communityLanguages={this.props.communityLanguages}
- onEdit={this.handleEditCommunity}
+ onUpsertCommunity={this.props.onEditCommunity}
onCancel={this.handleEditCancel}
enableNsfw={this.props.enableNsfw}
/>
@@ -124,30 +164,42 @@ export class Sidebar extends Component {
}
communityTitle() {
- let community = this.props.community_view.community;
- let subscribed = this.props.community_view.subscribed;
+ const community = this.props.community_view.community;
+ const subscribed = this.props.community_view.subscribed;
return (
{this.props.showIcon && !community.removed && (
)}
- {community.title}
+
+
+
{subscribed === "Subscribed" && (
-
- {i18n.t("joined")}
+ {this.state.followCommunityLoading ? (
+
+ ) : (
+ <>
+
+ {i18n.t("joined")}
+ >
+ )}
)}
{subscribed === "Pending" && (
- {i18n.t("subscribe_pending")}
+ {this.state.followCommunityLoading ? (
+
+ ) : (
+ i18n.t("subscribe_pending")
+ )}
)}
{community.removed && (
@@ -178,8 +230,8 @@ export class Sidebar extends Component {
}
badges() {
- let community_view = this.props.community_view;
- let counts = community_view.counts;
+ const community_view = this.props.community_view;
+ const counts = community_view.counts;
return (
@@ -284,7 +336,7 @@ export class Sidebar extends Component {
}
createPost() {
- let cv = this.props.community_view;
+ const cv = this.props.community_view;
return (
{
}
subscribe() {
- let community_view = this.props.community_view;
+ const community_view = this.props.community_view;
return (
{community_view.subscribed == "NotSubscribed" && (
- {i18n.t("subscribe")}
+ {this.state.followCommunityLoading ? (
+
+ ) : (
+ i18n.t("subscribe")
+ )}
)}
@@ -314,8 +370,8 @@ export class Sidebar extends Component {
}
blockCommunity() {
- let community_view = this.props.community_view;
- let blocked = this.props.community_view.blocked;
+ const community_view = this.props.community_view;
+ const blocked = this.props.community_view.blocked;
return (
@@ -323,16 +379,24 @@ export class Sidebar extends Component {
(blocked ? (
- {i18n.t("unblock_community")}
+ {this.state.blockCommunityLoading ? (
+
+ ) : (
+ i18n.t("unblock_community")
+ )}
) : (
- {i18n.t("block_community")}
+ {this.state.blockCommunityLoading ? (
+
+ ) : (
+ i18n.t("block_community")
+ )}
))}
@@ -340,7 +404,7 @@ export class Sidebar extends Component {
}
description() {
- let desc = this.props.community_view.community.description;
+ const desc = this.props.community_view.community.description;
return (
desc && (
@@ -349,7 +413,7 @@ export class Sidebar extends Component {
}
adminButtons() {
- let community_view = this.props.community_view;
+ const community_view = this.props.community_view;
return (
<>
@@ -386,7 +450,7 @@ export class Sidebar extends Component {
{i18n.t("yes")}
@@ -408,7 +472,7 @@ export class Sidebar extends Component {
{
: i18n.t("restore")
}
>
-
+ {this.state.deleteCommunityLoading ? (
+
+ ) : (
+
+ )}{" "}
)}
@@ -443,9 +511,13 @@ export class Sidebar extends Component {
) : (
- {i18n.t("restore")}
+ {this.state.removeCommunityLoading ? (
+
+ ) : (
+ i18n.t("restore")
+ )}
)}
{
)}
{this.state.showRemoveDialog && (
-
+
{i18n.t("reason")}
@@ -480,13 +552,17 @@ export class Sidebar extends Component {
{/*
*/}
- {i18n.t("remove_community")}
+ {this.state.removeCommunityLoading ? (
+
+ ) : (
+ i18n.t("remove_community")
+ )}
)}
{this.state.showPurgeDialog && (
-
+
@@ -504,7 +580,7 @@ export class Sidebar extends Component {
/>
- {this.state.purgeLoading ? (
+ {this.state.purgeCommunityLoading ? (
) : (
{
i.setState({ showEdit: true });
}
- handleEditCommunity() {
- this.setState({ showEdit: false });
- }
-
handleEditCancel() {
this.setState({ showEdit: false });
}
- handleDeleteClick(i: Sidebar, event: any) {
- event.preventDefault();
- let auth = myAuth();
- if (auth) {
- let deleteForm: DeleteCommunity = {
- community_id: i.props.community_view.community.id,
- deleted: !i.props.community_view.community.deleted,
- auth,
- };
- WebSocketService.Instance.send(wsClient.deleteCommunity(deleteForm));
- }
- }
-
handleShowConfirmLeaveModTeamClick(i: Sidebar) {
i.setState({ showConfirmLeaveModTeam: true });
}
- handleLeaveModTeamClick(i: Sidebar) {
- let mui = UserService.Instance.myUserInfo;
- let auth = myAuth();
- if (auth && mui) {
- let form: AddModToCommunity = {
- person_id: mui.local_user_view.person.id,
- community_id: i.props.community_view.community.id,
- added: false,
- auth,
- };
- WebSocketService.Instance.send(wsClient.addModToCommunity(form));
- i.setState({ showConfirmLeaveModTeam: false });
- }
- }
-
handleCancelLeaveModTeamClick(i: Sidebar) {
i.setState({ showConfirmLeaveModTeam: false });
}
- handleUnsubscribe(i: Sidebar, event: any) {
- event.preventDefault();
- let community_id = i.props.community_view.community.id;
- let auth = myAuth();
- if (auth) {
- let form: FollowCommunity = {
- community_id,
- follow: false,
- auth,
- };
- WebSocketService.Instance.send(wsClient.followCommunity(form));
- }
-
- // Update myUserInfo
- let mui = UserService.Instance.myUserInfo;
- if (mui) {
- mui.follows = mui.follows.filter(i => i.community.id != community_id);
- }
- }
-
- handleSubscribe(i: Sidebar, event: any) {
- event.preventDefault();
- let community_id = i.props.community_view.community.id;
- let auth = myAuth();
- if (auth) {
- let form: FollowCommunity = {
- community_id,
- follow: true,
- auth,
- };
- WebSocketService.Instance.send(wsClient.followCommunity(form));
- }
-
- // Update myUserInfo
- let mui = UserService.Instance.myUserInfo;
- if (mui) {
- mui.follows.push({
- community: i.props.community_view.community,
- follower: mui.local_user_view.person,
- });
- }
- }
-
get canPost(): boolean {
return (
!this.props.community_view.community.posting_restricted_to_mods ||
@@ -633,23 +634,6 @@ export class Sidebar extends Component {
i.setState({ removeExpires: event.target.value });
}
- handleModRemoveSubmit(i: Sidebar, event: any) {
- event.preventDefault();
- let auth = myAuth();
- if (auth) {
- let removeForm: RemoveCommunity = {
- community_id: i.props.community_view.community.id,
- removed: !i.props.community_view.community.removed,
- reason: i.state.removeReason,
- expires: getUnixTime(i.state.removeExpires),
- auth,
- };
- WebSocketService.Instance.send(wsClient.removeCommunity(removeForm));
-
- i.setState({ showRemoveDialog: false });
- }
- }
-
handlePurgeCommunityShow(i: Sidebar) {
i.setState({ showPurgeDialog: true, showRemoveDialog: false });
}
@@ -658,48 +642,75 @@ export class Sidebar extends Component {
i.setState({ purgeReason: event.target.value });
}
- handlePurgeSubmit(i: Sidebar, event: any) {
- event.preventDefault();
+ // TODO Do we need two of these?
+ handleUnfollowCommunity(i: Sidebar) {
+ i.setState({ followCommunityLoading: true });
+ i.props.onFollowCommunity({
+ community_id: i.props.community_view.community.id,
+ follow: false,
+ auth: myAuthRequired(),
+ });
+ }
- let auth = myAuth();
- if (auth) {
- let form: PurgeCommunity = {
+ handleFollowCommunity(i: Sidebar) {
+ i.setState({ followCommunityLoading: true });
+ i.props.onFollowCommunity({
+ community_id: i.props.community_view.community.id,
+ follow: true,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleBlockCommunity(i: Sidebar) {
+ i.setState({ blockCommunityLoading: true });
+ i.props.onBlockCommunity({
+ community_id: 0,
+ block: !i.props.community_view.blocked,
+ auth: myAuthRequired(),
+ });
+ }
+
+ handleLeaveModTeam(i: Sidebar) {
+ const myId = UserService.Instance.myUserInfo?.local_user_view.person.id;
+ if (myId) {
+ i.setState({ leaveModTeamLoading: true });
+ i.props.onLeaveModTeam({
community_id: i.props.community_view.community.id,
- reason: i.state.purgeReason,
- auth,
- };
- WebSocketService.Instance.send(wsClient.purgeCommunity(form));
- i.setState({ purgeLoading: true });
+ person_id: 92,
+ added: false,
+ auth: myAuthRequired(),
+ });
}
}
- handleBlock(i: Sidebar, event: any) {
- event.preventDefault();
- let auth = myAuth();
- if (auth) {
- let blockCommunityForm: BlockCommunity = {
- community_id: i.props.community_view.community.id,
- block: true,
- auth,
- };
- WebSocketService.Instance.send(
- wsClient.blockCommunity(blockCommunityForm)
- );
- }
+ handleDeleteCommunity(i: Sidebar) {
+ i.setState({ deleteCommunityLoading: true });
+ i.props.onDeleteCommunity({
+ community_id: i.props.community_view.community.id,
+ deleted: !i.props.community_view.community.deleted,
+ auth: myAuthRequired(),
+ });
}
- handleUnblock(i: Sidebar, event: any) {
+ handleRemoveCommunity(i: Sidebar, event: any) {
event.preventDefault();
- let auth = myAuth();
- if (auth) {
- let blockCommunityForm: BlockCommunity = {
- community_id: i.props.community_view.community.id,
- block: false,
- auth,
- };
- WebSocketService.Instance.send(
- wsClient.blockCommunity(blockCommunityForm)
- );
- }
+ i.setState({ removeCommunityLoading: true });
+ i.props.onRemoveCommunity({
+ community_id: i.props.community_view.community.id,
+ removed: !i.props.community_view.community.removed,
+ reason: i.state.removeReason,
+ expires: getUnixTime(i.state.removeExpires), // TODO fix this
+ auth: myAuthRequired(),
+ });
+ }
+
+ handlePurgeCommunity(i: Sidebar, event: any) {
+ event.preventDefault();
+ i.setState({ purgeCommunityLoading: true });
+ i.props.onPurgeCommunity({
+ community_id: i.props.community_view.community.id,
+ reason: i.state.purgeReason,
+ auth: myAuthRequired(),
+ });
}
}
diff --git a/src/shared/components/home/admin-settings.tsx b/src/shared/components/home/admin-settings.tsx
index 4419cf36..11be7257 100644
--- a/src/shared/components/home/admin-settings.tsx
+++ b/src/shared/components/home/admin-settings.tsx
@@ -1,30 +1,28 @@
-import autosize from "autosize";
import { Component, linkEvent } from "inferno";
import {
BannedPersonsResponse,
+ CreateCustomEmoji,
+ DeleteCustomEmoji,
+ EditCustomEmoji,
+ EditSite,
GetFederatedInstancesResponse,
GetSiteResponse,
PersonView,
- SiteResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces";
-import { WebSocketService } from "../../services";
+import { FirstLoadService } from "../../services/FirstLoadService";
+import { HttpService, RequestState } from "../../services/HttpService";
import {
- WithPromiseKeys,
+ RouteDataResponse,
capitalizeFirstLetter,
- isBrowser,
- myAuth,
- randomStr,
+ fetchThemeList,
+ myAuthRequired,
+ removeFromEmojiDataModel,
setIsoData,
showLocal,
toast,
- wsClient,
- wsSubscribe,
+ updateEmojiDataModel,
} from "../../utils";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
@@ -35,84 +33,76 @@ import RateLimitForm from "./rate-limit-form";
import { SiteForm } from "./site-form";
import { TaglineForm } from "./tagline-form";
-interface AdminSettingsData {
+type AdminSettingsData = RouteDataResponse<{
bannedPersonsResponse: BannedPersonsResponse;
federatedInstancesResponse: GetFederatedInstancesResponse;
-}
+}>;
interface AdminSettingsState {
siteRes: GetSiteResponse;
- instancesRes?: GetFederatedInstancesResponse;
banned: PersonView[];
- loading: boolean;
- leaveAdminTeamLoading: boolean;
+ currentTab: string;
+ instancesRes: RequestState;
+ bannedRes: RequestState;
+ leaveAdminTeamRes: RequestState;
+ themeList: string[];
+ isIsomorphic: boolean;
}
export class AdminSettings extends Component {
- private siteConfigTextAreaId = `site-config-${randomStr()}`;
private isoData = setIsoData(this.context);
- private subscription?: Subscription;
state: AdminSettingsState = {
siteRes: this.isoData.site_res,
banned: [],
- loading: true,
- leaveAdminTeamLoading: false,
+ currentTab: "site",
+ bannedRes: { state: "empty" },
+ instancesRes: { state: "empty" },
+ leaveAdminTeamRes: { state: "empty" },
+ themeList: [],
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
super(props, context);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleEditSite = this.handleEditSite.bind(this);
+ this.handleEditEmoji = this.handleEditEmoji.bind(this);
+ this.handleDeleteEmoji = this.handleDeleteEmoji.bind(this);
+ this.handleCreateEmoji = this.handleCreateEmoji.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path == this.context.router.route.match.url) {
- const { bannedPersonsResponse, federatedInstancesResponse } =
- this.isoData.routeData;
+ if (FirstLoadService.isFirstLoad) {
+ const {
+ bannedPersonsResponse: bannedRes,
+ federatedInstancesResponse: instancesRes,
+ } = this.isoData.routeData;
this.state = {
...this.state,
- banned: bannedPersonsResponse.banned,
- instancesRes: federatedInstancesResponse,
- loading: false,
+ bannedRes,
+ instancesRes,
+ isIsomorphic: true,
};
- } else {
- let cAuth = myAuth();
- if (cAuth) {
- WebSocketService.Instance.send(
- wsClient.getBannedPersons({
- auth: cAuth,
- })
- );
- WebSocketService.Instance.send(
- wsClient.getFederatedInstances({ auth: cAuth })
- );
- }
}
}
- static fetchInitialData({
+ static async fetchInitialData({
auth,
client,
- }: InitialFetchRequest): WithPromiseKeys {
+ }: InitialFetchRequest): Promise {
return {
- bannedPersonsResponse: client.getBannedPersons({ auth: auth as string }),
- federatedInstancesResponse: client.getFederatedInstances({
+ bannedPersonsResponse: await client.getBannedPersons({
auth: auth as string,
- }) as Promise,
+ }),
+ federatedInstancesResponse: await client.getFederatedInstances({
+ auth: auth as string,
+ }),
};
}
- componentDidMount() {
- if (isBrowser()) {
- var textarea: any = document.getElementById(this.siteConfigTextAreaId);
- autosize(textarea);
- }
- }
-
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchData();
}
}
@@ -123,78 +113,106 @@ export class AdminSettings extends Component {
}
render() {
+ const federationData =
+ this.state.instancesRes.state === "success"
+ ? this.state.instancesRes.data.federated_instances
+ : undefined;
+
return (
- {this.state.loading ? (
-
-
-
- ) : (
-
(
-
-
-
-
-
- {this.admins()}
- {this.bannedUsers()}
-
+
(
+
+
+
- ),
- },
- {
- key: "rate_limiting",
- label: "Rate Limiting",
- getNode: () => (
-
+ {this.admins()}
+ {this.bannedUsers()}
+
+
+ ),
+ },
+ {
+ key: "rate_limiting",
+ label: "Rate Limiting",
+ getNode: () => (
+
+ ),
+ },
+ {
+ key: "taglines",
+ label: i18n.t("taglines"),
+ getNode: () => (
+
+
- ),
- },
- {
- key: "taglines",
- label: i18n.t("taglines"),
- getNode: () => (
-
-
-
- ),
- },
- {
- key: "emojis",
- label: i18n.t("emojis"),
- getNode: () => (
-
-
-
- ),
- },
- ]}
- />
- )}
+
+ ),
+ },
+ {
+ key: "emojis",
+ label: i18n.t("emojis"),
+ getNode: () => (
+
+
+
+ ),
+ },
+ ]}
+ />
);
}
+ async fetchData() {
+ this.setState({
+ bannedRes: { state: "loading" },
+ instancesRes: { state: "loading" },
+ themeList: [],
+ });
+
+ const auth = myAuthRequired();
+
+ const [bannedRes, instancesRes, themeList] = await Promise.all([
+ HttpService.client.getBannedPersons({ auth }),
+ HttpService.client.getFederatedInstances({ auth }),
+ fetchThemeList(),
+ ]);
+
+ this.setState({
+ bannedRes,
+ instancesRes,
+ themeList,
+ });
+ }
+
admins() {
return (
<>
@@ -217,7 +235,7 @@ export class AdminSettings extends Component {
onClick={linkEvent(this, this.handleLeaveAdminTeam)}
className="btn btn-danger mb-2"
>
- {this.state.leaveAdminTeamLoading ? (
+ {this.state.leaveAdminTeamRes.state == "loading" ? (
) : (
i18n.t("leave_admin_team")
@@ -227,52 +245,83 @@ export class AdminSettings extends Component {
}
bannedUsers() {
- return (
- <>
- {i18n.t("banned_users")}
-
- {this.state.banned.map(banned => (
-
-
-
- ))}
-
- >
- );
- }
-
- handleLeaveAdminTeam(i: AdminSettings) {
- let auth = myAuth();
- if (auth) {
- i.setState({ leaveAdminTeamLoading: true });
- WebSocketService.Instance.send(wsClient.leaveAdmin({ auth }));
+ switch (this.state.bannedRes.state) {
+ case "loading":
+ return (
+
+
+
+ );
+ case "success": {
+ const bans = this.state.bannedRes.data.banned;
+ return (
+ <>
+ {i18n.t("banned_users")}
+
+ {bans.map(banned => (
+
+
+
+ ))}
+
+ >
+ );
+ }
}
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.context.router.history.push("/");
- this.setState({ loading: false });
- return;
- } else if (op == UserOperation.EditSite) {
- let data = wsJsonToRes(msg);
- this.setState(s => ((s.siteRes.site_view = data.site_view), s));
+ async handleEditSite(form: EditSite) {
+ const editRes = await HttpService.client.editSite(form);
+
+ if (editRes.state === "success") {
+ this.setState(s => {
+ s.siteRes.site_view = editRes.data.site_view;
+ // TODO: Where to get taglines from?
+ s.siteRes.taglines = editRes.data.taglines;
+ return s;
+ });
toast(i18n.t("site_saved"));
- } else if (op == UserOperation.GetBannedPersons) {
- let data = wsJsonToRes(msg);
- this.setState({ banned: data.banned, loading: false });
- } else if (op == UserOperation.LeaveAdmin) {
- let data = wsJsonToRes(msg);
- this.setState(s => ((s.siteRes.site_view = data.site_view), s));
- this.setState({ leaveAdminTeamLoading: false });
+ }
+
+ return editRes;
+ }
+
+ handleSwitchTab(i: { ctx: AdminSettings; tab: string }) {
+ i.ctx.setState({ currentTab: i.tab });
+ }
+
+ async handleLeaveAdminTeam(i: AdminSettings) {
+ i.setState({ leaveAdminTeamRes: { state: "loading" } });
+ this.setState({
+ leaveAdminTeamRes: await HttpService.client.leaveAdmin({
+ auth: myAuthRequired(),
+ }),
+ });
+
+ if (this.state.leaveAdminTeamRes.state === "success") {
toast(i18n.t("left_admin_team"));
- this.context.router.history.push("/");
- } else if (op == UserOperation.GetFederatedInstances) {
- let data = wsJsonToRes(msg);
- this.setState({ instancesRes: data });
+ this.context.router.history.replace("/");
+ }
+ }
+
+ async handleEditEmoji(form: EditCustomEmoji) {
+ const res = await HttpService.client.editCustomEmoji(form);
+ if (res.state === "success") {
+ updateEmojiDataModel(res.data.custom_emoji);
+ }
+ }
+
+ async handleDeleteEmoji(form: DeleteCustomEmoji) {
+ const res = await HttpService.client.deleteCustomEmoji(form);
+ if (res.state === "success") {
+ removeFromEmojiDataModel(res.data.id);
+ }
+ }
+
+ async handleCreateEmoji(form: CreateCustomEmoji) {
+ const res = await HttpService.client.createCustomEmoji(form);
+ if (res.state === "success") {
+ updateEmojiDataModel(res.data.custom_emoji);
}
}
}
diff --git a/src/shared/components/home/emojis-form.tsx b/src/shared/components/home/emojis-form.tsx
index d63633be..171b7c99 100644
--- a/src/shared/components/home/emojis-form.tsx
+++ b/src/shared/components/home/emojis-form.tsx
@@ -1,36 +1,30 @@
import { Component, linkEvent } from "inferno";
import {
CreateCustomEmoji,
- CustomEmojiResponse,
DeleteCustomEmoji,
- DeleteCustomEmojiResponse,
EditCustomEmoji,
GetSiteResponse,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
-import { WebSocketService } from "../../services";
+import { HttpService } from "../../services/HttpService";
import {
customEmojisLookup,
- isBrowser,
- myAuth,
+ myAuthRequired,
pictrsDeleteToast,
- removeFromEmojiDataModel,
setIsoData,
toast,
- updateEmojiDataModel,
- uploadImage,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { EmojiMart } from "../common/emoji-mart";
import { HtmlTags } from "../common/html-tags";
import { Icon } from "../common/icon";
import { Paginator } from "../common/paginator";
+interface EmojiFormProps {
+ onEdit(form: EditCustomEmoji): void;
+ onCreate(form: CreateCustomEmoji): void;
+ onDelete(form: DeleteCustomEmoji): void;
+}
+
interface EmojiFormState {
siteRes: GetSiteResponse;
customEmojis: CustomEmojiViewForm[];
@@ -49,9 +43,8 @@ interface CustomEmojiViewForm {
page: number;
}
-export class EmojiForm extends Component {
+export class EmojiForm extends Component {
private isoData = setIsoData(this.context);
- private subscription: Subscription | undefined;
private itemsPerPage = 15;
private emptyState: EmojiFormState = {
loading: false,
@@ -75,20 +68,12 @@ export class EmojiForm extends Component {
this.state = this.emptyState;
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
this.handleEmojiClick = this.handleEmojiClick.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
}
get documentTitle(): string {
return i18n.t("custom_emojis");
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
- }
- }
-
render() {
return (
@@ -232,7 +217,7 @@ export class EmojiForm extends Component
{
"btn btn-link btn-animate"
}
onClick={linkEvent(
- { form: this, cv: cv },
+ { i: this, cv: cv },
this.handleEditEmojiClick
)}
data-tippy-content={i18n.t("save")}
@@ -253,7 +238,7 @@ export class EmojiForm extends Component {
{
props: { form: EmojiForm; index: number },
event: any
) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex =
+ const custom_emojis = [...props.form.state.customEmojis];
+ const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- let item = {
+ const item = {
...props.form.state.customEmojis[pagedIndex],
category: event.target.value,
changed: true,
@@ -341,10 +326,10 @@ export class EmojiForm extends Component {
props: { form: EmojiForm; index: number },
event: any
) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex =
+ const custom_emojis = [...props.form.state.customEmojis];
+ const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- let item = {
+ const item = {
...props.form.state.customEmojis[pagedIndex],
shortcode: event.target.value,
changed: true,
@@ -357,10 +342,10 @@ export class EmojiForm extends Component {
props: { form: EmojiForm; index: number; overrideValue: string | null },
event: any
) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex =
+ const custom_emojis = [...props.form.state.customEmojis];
+ const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- let item = {
+ const item = {
...props.form.state.customEmojis[pagedIndex],
image_url: props.overrideValue ?? event.target.value,
changed: true,
@@ -373,10 +358,10 @@ export class EmojiForm extends Component {
props: { form: EmojiForm; index: number },
event: any
) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex =
+ const custom_emojis = [...props.form.state.customEmojis];
+ const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- let item = {
+ const item = {
...props.form.state.customEmojis[pagedIndex],
alt_text: event.target.value,
changed: true,
@@ -389,10 +374,10 @@ export class EmojiForm extends Component {
props: { form: EmojiForm; index: number },
event: any
) {
- let custom_emojis = [...props.form.state.customEmojis];
- let pagedIndex =
+ const custom_emojis = [...props.form.state.customEmojis];
+ const pagedIndex =
(props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- let item = {
+ const item = {
...props.form.state.customEmojis[pagedIndex],
keywords: event.target.value,
changed: true,
@@ -401,60 +386,56 @@ export class EmojiForm extends Component {
props.form.setState({ customEmojis: custom_emojis });
}
- handleDeleteEmojiClick(props: {
- form: EmojiForm;
+ handleDeleteEmojiClick(d: {
+ i: EmojiForm;
index: number;
cv: CustomEmojiViewForm;
}) {
- let pagedIndex =
- (props.form.state.page - 1) * props.form.itemsPerPage + props.index;
- if (props.cv.id != 0) {
- const deleteForm: DeleteCustomEmoji = {
- id: props.cv.id,
- auth: myAuth() ?? "",
- };
- WebSocketService.Instance.send(wsClient.deleteCustomEmoji(deleteForm));
+ const pagedIndex = (d.i.state.page - 1) * d.i.itemsPerPage + d.index;
+ if (d.cv.id != 0) {
+ d.i.props.onDelete({
+ id: d.cv.id,
+ auth: myAuthRequired(),
+ });
} else {
- let custom_emojis = [...props.form.state.customEmojis];
+ const custom_emojis = [...d.i.state.customEmojis];
custom_emojis.splice(Number(pagedIndex), 1);
- props.form.setState({ customEmojis: custom_emojis });
+ d.i.setState({ customEmojis: custom_emojis });
}
}
- handleEditEmojiClick(props: { form: EmojiForm; cv: CustomEmojiViewForm }) {
- const keywords = props.cv.keywords
+ handleEditEmojiClick(d: { i: EmojiForm; cv: CustomEmojiViewForm }) {
+ const keywords = d.cv.keywords
.split(" ")
.filter(x => x.length > 0) as string[];
const uniqueKeywords = Array.from(new Set(keywords));
- if (props.cv.id != 0) {
- const editForm: EditCustomEmoji = {
- id: props.cv.id,
- category: props.cv.category,
- image_url: props.cv.image_url,
- alt_text: props.cv.alt_text,
+ if (d.cv.id != 0) {
+ d.i.props.onEdit({
+ id: d.cv.id,
+ category: d.cv.category,
+ image_url: d.cv.image_url,
+ alt_text: d.cv.alt_text,
keywords: uniqueKeywords,
- auth: myAuth() ?? "",
- };
- WebSocketService.Instance.send(wsClient.editCustomEmoji(editForm));
+ auth: myAuthRequired(),
+ });
} else {
- const createForm: CreateCustomEmoji = {
- category: props.cv.category,
- shortcode: props.cv.shortcode,
- image_url: props.cv.image_url,
- alt_text: props.cv.alt_text,
+ d.i.props.onCreate({
+ category: d.cv.category,
+ shortcode: d.cv.shortcode,
+ image_url: d.cv.image_url,
+ alt_text: d.cv.alt_text,
keywords: uniqueKeywords,
- auth: myAuth() ?? "",
- };
- WebSocketService.Instance.send(wsClient.createCustomEmoji(createForm));
+ auth: myAuthRequired(),
+ });
}
}
handleAddEmojiClick(form: EmojiForm, event: any) {
event.preventDefault();
- let custom_emojis = [...form.state.customEmojis];
+ const custom_emojis = [...form.state.customEmojis];
const page =
1 + Math.floor(form.state.customEmojis.length / form.itemsPerPage);
- let item: CustomEmojiViewForm = {
+ const item: CustomEmojiViewForm = {
id: 0,
shortcode: "",
alt_text: "",
@@ -477,26 +458,26 @@ export class EmojiForm extends Component {
file = event;
}
- uploadImage(file)
- .then(res => {
- console.log("pictrs upload:");
- console.log(res);
- if (res.msg === "ok") {
- pictrsDeleteToast(file.name, res.delete_url as string);
+ HttpService.client.uploadImage({ image: file }).then(res => {
+ console.log("pictrs upload:");
+ console.log(res);
+ if (res.state === "success") {
+ if (res.data.msg === "ok") {
+ pictrsDeleteToast(file.name, res.data.delete_url as string);
} else {
toast(JSON.stringify(res), "danger");
- let hash = res.files?.at(0)?.file;
- let url = `${res.url}/${hash}`;
+ const hash = res.data.files?.at(0)?.file;
+ const url = `${res.data.url}/${hash}`;
props.form.handleEmojiImageUrlChange(
{ form: props.form, index: props.index, overrideValue: url },
event
);
}
- })
- .catch(error => {
- console.error(error);
- toast(error, "danger");
- });
+ } else if (res.state === "failed") {
+ console.error(res.msg);
+ toast(res.msg, "danger");
+ }
+ });
}
configurePicker(): any {
@@ -506,51 +487,4 @@ export class EmojiForm extends Component {
dynamicWidth: true,
};
}
-
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.context.router.history.push("/");
- this.setState({ loading: false });
- return;
- } else if (op == UserOperation.CreateCustomEmoji) {
- let data = wsJsonToRes(msg);
- const custom_emoji_view = data.custom_emoji;
- updateEmojiDataModel(custom_emoji_view);
- let currentEmojis = this.state.customEmojis;
- let newEmojiIndex = currentEmojis.findIndex(
- x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
- );
- currentEmojis[newEmojiIndex].id = custom_emoji_view.custom_emoji.id;
- currentEmojis[newEmojiIndex].changed = false;
- this.setState({ customEmojis: currentEmojis });
- toast(i18n.t("saved_emoji"));
- this.setState({ loading: false });
- } else if (op == UserOperation.EditCustomEmoji) {
- let data = wsJsonToRes(msg);
- const custom_emoji_view = data.custom_emoji;
- updateEmojiDataModel(data.custom_emoji);
- let currentEmojis = this.state.customEmojis;
- let newEmojiIndex = currentEmojis.findIndex(
- x => x.shortcode == custom_emoji_view.custom_emoji.shortcode
- );
- currentEmojis[newEmojiIndex].changed = false;
- this.setState({ customEmojis: currentEmojis });
- toast(i18n.t("saved_emoji"));
- this.setState({ loading: false });
- } else if (op == UserOperation.DeleteCustomEmoji) {
- let data = wsJsonToRes(msg);
- if (data.success) {
- removeFromEmojiDataModel(data.id);
- let custom_emojis = [
- ...this.state.customEmojis.filter(x => x.id != data.id),
- ];
- this.setState({ customEmojis: custom_emojis });
- toast(i18n.t("deleted_emoji"));
- }
- this.setState({ loading: false });
- }
- }
}
diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx
index e85c3e66..cc9dd518 100644
--- a/src/shared/components/home/home.tsx
+++ b/src/shared/components/home/home.tsx
@@ -3,13 +3,27 @@ import { Component, linkEvent, MouseEventHandler } from "inferno";
import { T } from "inferno-i18next-dess";
import { Link } from "inferno-router";
import {
- AddAdminResponse,
+ AddAdmin,
+ AddModToCommunity,
+ BanFromCommunity,
+ BanFromCommunityResponse,
+ BanPerson,
BanPersonResponse,
- BlockPersonResponse,
- CommentReportResponse,
+ BlockPerson,
+ CommentId,
+ CommentReplyResponse,
CommentResponse,
- CommentView,
- CommunityView,
+ CreateComment,
+ CreateCommentLike,
+ CreateCommentReport,
+ CreatePostLike,
+ CreatePostReport,
+ DeleteComment,
+ DeletePost,
+ DistinguishComment,
+ EditComment,
+ EditPost,
+ FeaturePost,
GetComments,
GetCommentsResponse,
GetPosts,
@@ -18,50 +32,52 @@ import {
ListCommunities,
ListCommunitiesResponse,
ListingType,
- PostReportResponse,
+ LockPost,
+ MarkCommentReplyAsRead,
+ MarkPersonMentionAsRead,
PostResponse,
- PostView,
+ PurgeComment,
PurgeItemResponse,
- SiteResponse,
+ PurgePerson,
+ PurgePost,
+ RemoveComment,
+ RemovePost,
+ SaveComment,
+ SavePost,
SortType,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
+ TransferCommunity,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import {
CommentViewType,
DataType,
InitialFetchRequest,
} from "../../interfaces";
-import { UserService, WebSocketService } from "../../services";
+import { UserService } from "../../services";
+import { FirstLoadService } from "../../services/FirstLoadService";
+import { HttpService, RequestState } from "../../services/HttpService";
import {
canCreateCommunity,
commentsToFlatNodes,
- createCommentLikeRes,
- createPostLikeFindRes,
- editCommentRes,
- editPostFindRes,
+ editComment,
+ editPost,
+ editWith,
enableDownvotes,
enableNsfw,
fetchLimit,
+ getCommentParentId,
getDataTypeString,
getPageFromString,
getQueryParams,
getQueryString,
getRandomFromList,
- isBrowser,
- isPostBlocked,
mdToHtml,
myAuth,
- notifyPost,
- nsfwCheck,
postToCommentSortType,
QueryParams,
relTags,
restoreScrollPosition,
- saveCommentRes,
+ RouteDataResponse,
saveScrollPosition,
setIsoData,
setupTippy,
@@ -69,9 +85,6 @@ import {
toast,
trendingFetchLimit,
updatePersonBlock,
- WithPromiseKeys,
- wsClient,
- wsSubscribe,
} from "../../utils";
import { CommentNodes } from "../comment/comment-nodes";
import { DataTypeSelect } from "../common/data-type-select";
@@ -85,16 +98,17 @@ import { PostListings } from "../post/post-listings";
import { SiteSidebar } from "./site-sidebar";
interface HomeState {
- trendingCommunities: CommunityView[];
- siteRes: GetSiteResponse;
- posts: PostView[];
- comments: CommentView[];
+ postsRes: RequestState;
+ commentsRes: RequestState;
+ trendingCommunitiesRes: RequestState;
showSubscribedMobile: boolean;
showTrendingMobile: boolean;
showSidebarMobile: boolean;
subscribedCollapsed: boolean;
- loading: boolean;
tagline?: string;
+ siteRes: GetSiteResponse;
+ finished: Map;
+ isIsomorphic: boolean;
}
interface HomeProps {
@@ -104,11 +118,11 @@ interface HomeProps {
page: number;
}
-interface HomeData {
+type HomeData = RouteDataResponse<{
postsResponse?: GetPostsResponse;
commentsResponse?: GetCommentsResponse;
trendingResponse: ListCommunitiesResponse;
-}
+}>;
function getDataTypeFromQuery(type?: string): DataType {
return type ? DataType[type] : DataType.Post;
@@ -119,7 +133,7 @@ function getListingTypeFromQuery(type?: string): ListingType {
UserService.Instance.myUserInfo?.local_user_view?.local_user
?.default_listing_type;
- return type ? (type as ListingType) : myListingType ?? "Local";
+ return (type ? (type as ListingType) : myListingType) ?? "Local";
}
function getSortTypeFromQuery(type?: string): SortType {
@@ -127,7 +141,7 @@ function getSortTypeFromQuery(type?: string): SortType {
UserService.Instance.myUserInfo?.local_user_view?.local_user
?.default_sort_type;
- return type ? (type as SortType) : mySortType ?? "Active";
+ return (type ? (type as SortType) : mySortType) ?? "Active";
}
const getHomeQueryParams = () =>
@@ -138,48 +152,6 @@ const getHomeQueryParams = () =>
dataType: getDataTypeFromQuery,
});
-function fetchTrendingCommunities() {
- const listCommunitiesForm: ListCommunities = {
- type_: "Local",
- sort: "Hot",
- limit: trendingFetchLimit,
- auth: myAuth(false),
- };
- WebSocketService.Instance.send(wsClient.listCommunities(listCommunitiesForm));
-}
-
-function fetchData() {
- const auth = myAuth(false);
- const { dataType, page, listingType, sort } = getHomeQueryParams();
- let req: string;
-
- if (dataType === DataType.Post) {
- const getPostsForm: GetPosts = {
- page,
- limit: fetchLimit,
- sort,
- saved_only: false,
- type_: listingType,
- auth,
- };
-
- req = wsClient.getPosts(getPostsForm);
- } else {
- const getCommentsForm: GetComments = {
- page,
- limit: fetchLimit,
- sort: postToCommentSortType(sort),
- saved_only: false,
- type_: listingType,
- auth,
- };
-
- req = wsClient.getComments(getCommentsForm);
- }
-
- WebSocketService.Instance.send(req);
-}
-
const MobileButton = ({
textKey,
show,
@@ -210,52 +182,19 @@ const LinkButton = ({
);
-function getRss(listingType: ListingType) {
- const { sort } = getHomeQueryParams();
- const auth = myAuth(false);
-
- let rss: string | undefined = undefined;
-
- switch (listingType) {
- case "All": {
- rss = `/feeds/all.xml?sort=${sort}`;
- break;
- }
- case "Local": {
- rss = `/feeds/local.xml?sort=${sort}`;
- break;
- }
- case "Subscribed": {
- rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
- break;
- }
- }
-
- return (
- rss && (
- <>
-
-
-
-
- >
- )
- );
-}
-
export class Home extends Component {
private isoData = setIsoData(this.context);
- private subscription?: Subscription;
state: HomeState = {
- trendingCommunities: [],
+ postsRes: { state: "empty" },
+ commentsRes: { state: "empty" },
+ trendingCommunitiesRes: { state: "empty" },
siteRes: this.isoData.site_res,
showSubscribedMobile: false,
showTrendingMobile: false,
showSidebarMobile: false,
subscribedCollapsed: false,
- loading: true,
- posts: [],
- comments: [],
+ finished: new Map(),
+ isIsomorphic: false,
};
constructor(props: any, context: any) {
@@ -266,58 +205,82 @@ export class Home extends Component {
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
this.handlePageChange = this.handlePageChange.bind(this);
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
+ this.handleCreateComment = this.handleCreateComment.bind(this);
+ this.handleEditComment = this.handleEditComment.bind(this);
+ this.handleSaveComment = this.handleSaveComment.bind(this);
+ this.handleBlockPerson = this.handleBlockPerson.bind(this);
+ this.handleDeleteComment = this.handleDeleteComment.bind(this);
+ this.handleRemoveComment = this.handleRemoveComment.bind(this);
+ this.handleCommentVote = this.handleCommentVote.bind(this);
+ this.handleAddModToCommunity = this.handleAddModToCommunity.bind(this);
+ this.handleAddAdmin = this.handleAddAdmin.bind(this);
+ this.handlePurgePerson = this.handlePurgePerson.bind(this);
+ this.handlePurgeComment = this.handlePurgeComment.bind(this);
+ this.handleCommentReport = this.handleCommentReport.bind(this);
+ this.handleDistinguishComment = this.handleDistinguishComment.bind(this);
+ this.handleTransferCommunity = this.handleTransferCommunity.bind(this);
+ this.handleCommentReplyRead = this.handleCommentReplyRead.bind(this);
+ this.handlePersonMentionRead = this.handlePersonMentionRead.bind(this);
+ this.handleBanFromCommunity = this.handleBanFromCommunity.bind(this);
+ this.handleBanPerson = this.handleBanPerson.bind(this);
+ this.handlePostEdit = this.handlePostEdit.bind(this);
+ this.handlePostVote = this.handlePostVote.bind(this);
+ this.handlePostReport = this.handlePostReport.bind(this);
+ this.handleLockPost = this.handleLockPost.bind(this);
+ this.handleDeletePost = this.handleDeletePost.bind(this);
+ this.handleRemovePost = this.handleRemovePost.bind(this);
+ this.handleSavePost = this.handleSavePost.bind(this);
+ this.handlePurgePost = this.handlePurgePost.bind(this);
+ this.handleFeaturePost = this.handleFeaturePost.bind(this);
// Only fetch the data if coming from another route
- if (this.isoData.path === this.context.router.route.match.url) {
- const { trendingResponse, commentsResponse, postsResponse } =
- this.isoData.routeData;
+ if (FirstLoadService.isFirstLoad) {
+ const {
+ trendingResponse: trendingCommunitiesRes,
+ commentsResponse: commentsRes,
+ postsResponse: postsRes,
+ } = this.isoData.routeData;
- if (postsResponse) {
- this.state = { ...this.state, posts: postsResponse.posts };
- }
-
- if (commentsResponse) {
- this.state = { ...this.state, comments: commentsResponse.comments };
- }
-
- if (isBrowser()) {
- WebSocketService.Instance.send(
- wsClient.communityJoin({ community_id: 0 })
- );
- }
- const taglines = this.state?.siteRes?.taglines ?? [];
this.state = {
...this.state,
- trendingCommunities: trendingResponse?.communities ?? [],
- loading: false,
- tagline: getRandomFromList(taglines)?.content,
+ trendingCommunitiesRes,
+ tagline: getRandomFromList(this.state?.siteRes?.taglines ?? [])
+ ?.content,
+ isIsomorphic: true,
};
- } else {
- fetchTrendingCommunities();
- fetchData();
+
+ if (commentsRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ commentsRes,
+ };
+ }
+
+ if (postsRes?.state === "success") {
+ this.state = {
+ ...this.state,
+ postsRes,
+ };
+ }
}
}
- componentDidMount() {
- // This means it hasn't been set up yet
- if (!this.state.siteRes.site_view.local_site.site_setup) {
- this.context.router.history.push("/setup");
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await Promise.all([this.fetchTrendingCommunities(), this.fetchData()]);
}
setupTippy();
}
componentWillUnmount() {
saveScrollPosition(this.context);
- this.subscription?.unsubscribe();
}
- static fetchInitialData({
+ static async fetchInitialData({
client,
auth,
query: { dataType: urlDataType, listingType, page: urlPage, sort: urlSort },
- }: InitialFetchRequest>): WithPromiseKeys {
+ }: InitialFetchRequest>): Promise {
const dataType = getDataTypeFromQuery(urlDataType);
// TODO figure out auth default_listingType, default_sort_type
@@ -326,10 +289,9 @@ export class Home extends Component {
const page = urlPage ? Number(urlPage) : 1;
- const promises: Promise[] = [];
-
- let postsResponse: Promise | undefined = undefined;
- let commentsResponse: Promise | undefined = undefined;
+ let postsResponse: RequestState | undefined = undefined;
+ let commentsResponse: RequestState | undefined =
+ undefined;
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
@@ -341,7 +303,7 @@ export class Home extends Component {
auth,
};
- postsResponse = client.getPosts(getPostsForm);
+ postsResponse = await client.getPosts(getPostsForm);
} else {
const getCommentsForm: GetComments = {
page,
@@ -352,7 +314,7 @@ export class Home extends Component {
auth,
};
- commentsResponse = client.getComments(getCommentsForm);
+ commentsResponse = await client.getComments(getCommentsForm);
}
const trendingCommunitiesForm: ListCommunities = {
@@ -361,10 +323,9 @@ export class Home extends Component {
limit: trendingFetchLimit,
auth,
};
- promises.push(client.listCommunities(trendingCommunitiesForm));
return {
- trendingResponse: client.listCommunities(trendingCommunitiesForm),
+ trendingResponse: await client.listCommunities(trendingCommunitiesForm),
commentsResponse,
postsResponse,
};
@@ -481,69 +442,77 @@ export class Home extends Component {
admins,
online,
},
- loading,
} = this.state;
return (
- {!loading && (
-
-
-
- {this.trendingCommunities()}
- {canCreateCommunity(this.state.siteRes) && (
-
- )}
+
+
+
+ {this.trendingCommunities()}
+ {canCreateCommunity(this.state.siteRes) && (
-
+ )}
+
-
- {this.hasFollows && (
-
-
{this.subscribedCommunities}
-
- )}
- )}
+
+ {this.hasFollows && (
+
+
{this.subscribedCommunities}
+
+ )}
+
);
}
trendingCommunities(isMobile = false) {
- return (
-
-
-
- #
-
- #
-
-
-
-
- {this.state.trendingCommunities.map(cv => (
-
-
-
- ))}
-
-
- );
+ switch (this.state.trendingCommunitiesRes.state) {
+ case "loading":
+ return (
+
+
+
+ );
+ case "success": {
+ const trending = this.state.trendingCommunitiesRes.data.communities;
+ return (
+
+
+
+ #
+
+ #
+
+
+
+
+ {trending.map(cv => (
+
+
+
+ ))}
+
+
+ );
+ }
+ }
}
get subscribedCommunities() {
@@ -586,7 +555,7 @@ export class Home extends Component
{
);
}
- updateUrl({ dataType, listingType, page, sort }: Partial) {
+ async updateUrl({ dataType, listingType, page, sort }: Partial) {
const {
dataType: urlDataType,
listingType: urlListingType,
@@ -606,13 +575,7 @@ export class Home extends Component {
search: getQueryString(queryParams),
});
- this.setState({
- loading: true,
- posts: [],
- comments: [],
- });
-
- fetchData();
+ await this.fetchData();
}
posts() {
@@ -620,50 +583,105 @@ export class Home extends Component {
return (
- {this.state.loading ? (
-
-
-
- ) : (
-
- {this.selects()}
- {this.listings}
-
-
- )}
+
+ {this.selects}
+ {this.listings}
+
+
);
}
get listings() {
const { dataType } = getHomeQueryParams();
- const { siteRes, posts, comments } = this.state;
+ const siteRes = this.state.siteRes;
- return dataType === DataType.Post ? (
-
- ) : (
-
- );
+ if (dataType === DataType.Post) {
+ switch (this.state.postsRes.state) {
+ case "loading":
+ return (
+
+
+
+ );
+ case "success": {
+ const posts = this.state.postsRes.data.posts;
+ return (
+
+ );
+ }
+ }
+ } else {
+ switch (this.state.commentsRes.state) {
+ case "loading":
+ return (
+
+
+
+ );
+ case "success": {
+ const comments = this.state.commentsRes.data.comments;
+ return (
+
+ );
+ }
+ }
+ }
}
- selects() {
+ get selects() {
const { listingType, dataType, sort } = getHomeQueryParams();
return (
@@ -685,11 +703,90 @@ export class Home extends Component {
- {getRss(listingType)}
+ {this.getRss(listingType)}
);
}
+ getRss(listingType: ListingType) {
+ const { sort } = getHomeQueryParams();
+ const auth = myAuth();
+
+ let rss: string | undefined = undefined;
+
+ switch (listingType) {
+ case "All": {
+ rss = `/feeds/all.xml?sort=${sort}`;
+ break;
+ }
+ case "Local": {
+ rss = `/feeds/local.xml?sort=${sort}`;
+ break;
+ }
+ case "Subscribed": {
+ rss = auth ? `/feeds/front/${auth}.xml?sort=${sort}` : undefined;
+ break;
+ }
+ }
+
+ return (
+ rss && (
+ <>
+
+
+
+
+ >
+ )
+ );
+ }
+
+ async fetchTrendingCommunities() {
+ this.setState({ trendingCommunitiesRes: { state: "loading" } });
+ this.setState({
+ trendingCommunitiesRes: await HttpService.client.listCommunities({
+ type_: "Local",
+ sort: "Hot",
+ limit: trendingFetchLimit,
+ auth: myAuth(),
+ }),
+ });
+ }
+
+ async fetchData() {
+ const auth = myAuth();
+ const { dataType, page, listingType, sort } = getHomeQueryParams();
+
+ if (dataType === DataType.Post) {
+ this.setState({ postsRes: { state: "loading" } });
+ this.setState({
+ postsRes: await HttpService.client.getPosts({
+ page,
+ limit: fetchLimit,
+ sort,
+ saved_only: false,
+ type_: listingType,
+ auth,
+ }),
+ });
+ } else {
+ this.setState({ commentsRes: { state: "loading" } });
+ this.setState({
+ commentsRes: await HttpService.client.getComments({
+ page,
+ limit: fetchLimit,
+ sort: postToCommentSortType(sort),
+ saved_only: false,
+ type_: listingType,
+ auth,
+ }),
+ });
+ }
+
+ restoreScrollPosition(this.context);
+ setupTippy();
+ }
+
handleShowSubscribedMobile(i: Home) {
i.setState({ showSubscribedMobile: !i.state.showSubscribedMobile });
}
@@ -726,229 +823,252 @@ export class Home extends Component
{
window.scrollTo(0, 0);
}
- parseMessage(msg: any) {
- const op = wsUserOp(msg);
- console.log(msg);
+ async handleAddModToCommunity(form: AddModToCommunity) {
+ // TODO not sure what to do here
+ await HttpService.client.addModToCommunity(form);
+ }
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- } else if (msg.reconnect) {
- WebSocketService.Instance.send(
- wsClient.communityJoin({ community_id: 0 })
- );
- fetchData();
- } else {
- switch (op) {
- case UserOperation.ListCommunities: {
- const { communities } = wsJsonToRes(msg);
- this.setState({ trendingCommunities: communities });
+ async handlePurgePerson(form: PurgePerson) {
+ const purgePersonRes = await HttpService.client.purgePerson(form);
+ this.purgeItem(purgePersonRes);
+ }
- break;
- }
+ async handlePurgeComment(form: PurgeComment) {
+ const purgeCommentRes = await HttpService.client.purgeComment(form);
+ this.purgeItem(purgeCommentRes);
+ }
- case UserOperation.EditSite: {
- const { site_view } = wsJsonToRes(msg);
- this.setState(s => ((s.siteRes.site_view = site_view), s));
- toast(i18n.t("site_saved"));
+ async handlePurgePost(form: PurgePost) {
+ const purgeRes = await HttpService.client.purgePost(form);
+ this.purgeItem(purgeRes);
+ }
- break;
- }
-
- case UserOperation.GetPosts: {
- const { posts } = wsJsonToRes(msg);
- this.setState({ posts, loading: false });
- WebSocketService.Instance.send(
- wsClient.communityJoin({ community_id: 0 })
- );
- restoreScrollPosition(this.context);
- setupTippy();
-
- break;
- }
-
- case UserOperation.CreatePost: {
- const { page, listingType } = getHomeQueryParams();
- const { post_view } = wsJsonToRes(msg);
-
- // Only push these if you're on the first page, you pass the nsfw check, and it isn't blocked
- if (page === 1 && nsfwCheck(post_view) && !isPostBlocked(post_view)) {
- const mui = UserService.Instance.myUserInfo;
- const showPostNotifs =
- mui?.local_user_view.local_user.show_new_post_notifs;
- let shouldAddPost: boolean;
-
- switch (listingType) {
- case "Subscribed": {
- // If you're on subscribed, only push it if you're subscribed.
- shouldAddPost = !!mui?.follows.some(
- ({ community: { id } }) => id === post_view.community.id
- );
- break;
- }
- case "Local": {
- // If you're on the local view, only push it if its local
- shouldAddPost = post_view.post.local;
- break;
- }
- default: {
- shouldAddPost = true;
- break;
- }
- }
-
- if (shouldAddPost) {
- this.setState(({ posts }) => ({
- posts: [post_view].concat(posts),
- }));
- if (showPostNotifs) {
- notifyPost(post_view, this.context.router);
- }
- }
- }
-
- break;
- }
-
- case UserOperation.EditPost:
- case UserOperation.DeletePost:
- case UserOperation.RemovePost:
- case UserOperation.LockPost:
- case UserOperation.FeaturePost:
- case UserOperation.SavePost: {
- const { post_view } = wsJsonToRes(msg);
- editPostFindRes(post_view, this.state.posts);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.CreatePostLike: {
- const { post_view } = wsJsonToRes(msg);
- createPostLikeFindRes(post_view, this.state.posts);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.AddAdmin: {
- const { admins } = wsJsonToRes(msg);
- this.setState(s => ((s.siteRes.admins = admins), s));
-
- break;
- }
-
- case UserOperation.BanPerson: {
- const {
- banned,
- person_view: {
- person: { id },
- },
- } = wsJsonToRes(msg);
-
- this.state.posts
- .filter(p => p.creator.id == id)
- .forEach(p => (p.creator.banned = banned));
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.GetComments: {
- const { comments } = wsJsonToRes(msg);
- this.setState({ comments, loading: false });
-
- break;
- }
-
- case UserOperation.EditComment:
- case UserOperation.DeleteComment:
- case UserOperation.RemoveComment: {
- const { comment_view } = wsJsonToRes(msg);
- editCommentRes(comment_view, this.state.comments);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.CreateComment: {
- const { form_id, comment_view } = wsJsonToRes(msg);
-
- // Necessary since it might be a user reply
- if (form_id) {
- const { listingType } = getHomeQueryParams();
-
- // If you're on subscribed, only push it if you're subscribed.
- const shouldAddComment =
- listingType === "Subscribed"
- ? UserService.Instance.myUserInfo?.follows.some(
- ({ community: { id } }) => id === comment_view.community.id
- )
- : true;
-
- if (shouldAddComment) {
- this.setState(({ comments }) => ({
- comments: [comment_view].concat(comments),
- }));
- }
- }
-
- break;
- }
-
- case UserOperation.SaveComment: {
- const { comment_view } = wsJsonToRes(msg);
- saveCommentRes(comment_view, this.state.comments);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.CreateCommentLike: {
- const { comment_view } = wsJsonToRes(msg);
- createCommentLikeRes(comment_view, this.state.comments);
- this.setState(this.state);
-
- break;
- }
-
- case UserOperation.BlockPerson: {
- const data = wsJsonToRes(msg);
- updatePersonBlock(data);
-
- break;
- }
-
- case UserOperation.CreatePostReport: {
- const data = wsJsonToRes(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
-
- break;
- }
-
- case UserOperation.CreateCommentReport: {
- const data = wsJsonToRes(msg);
- if (data) {
- toast(i18n.t("report_created"));
- }
-
- break;
- }
-
- case UserOperation.PurgePerson:
- case UserOperation.PurgePost:
- case UserOperation.PurgeComment:
- case UserOperation.PurgeCommunity: {
- const data = wsJsonToRes(msg);
- if (data.success) {
- toast(i18n.t("purge_success"));
- this.context.router.history.push(`/`);
- }
-
- break;
- }
- }
+ async handleBlockPerson(form: BlockPerson) {
+ const blockPersonRes = await HttpService.client.blockPerson(form);
+ if (blockPersonRes.state == "success") {
+ updatePersonBlock(blockPersonRes.data);
}
}
+
+ async handleCreateComment(form: CreateComment) {
+ const createCommentRes = await HttpService.client.createComment(form);
+ this.createAndUpdateComments(createCommentRes);
+
+ return createCommentRes;
+ }
+
+ async handleEditComment(form: EditComment) {
+ const editCommentRes = await HttpService.client.editComment(form);
+ this.findAndUpdateComment(editCommentRes);
+
+ return editCommentRes;
+ }
+
+ async handleDeleteComment(form: DeleteComment) {
+ const deleteCommentRes = await HttpService.client.deleteComment(form);
+ this.findAndUpdateComment(deleteCommentRes);
+ }
+
+ async handleDeletePost(form: DeletePost) {
+ const deleteRes = await HttpService.client.deletePost(form);
+ this.findAndUpdatePost(deleteRes);
+ }
+
+ async handleRemovePost(form: RemovePost) {
+ const removeRes = await HttpService.client.removePost(form);
+ this.findAndUpdatePost(removeRes);
+ }
+
+ async handleRemoveComment(form: RemoveComment) {
+ const removeCommentRes = await HttpService.client.removeComment(form);
+ this.findAndUpdateComment(removeCommentRes);
+ }
+
+ async handleSaveComment(form: SaveComment) {
+ const saveCommentRes = await HttpService.client.saveComment(form);
+ this.findAndUpdateComment(saveCommentRes);
+ }
+
+ async handleSavePost(form: SavePost) {
+ const saveRes = await HttpService.client.savePost(form);
+ this.findAndUpdatePost(saveRes);
+ }
+
+ async handleFeaturePost(form: FeaturePost) {
+ const featureRes = await HttpService.client.featurePost(form);
+ this.findAndUpdatePost(featureRes);
+ }
+
+ async handleCommentVote(form: CreateCommentLike) {
+ const voteRes = await HttpService.client.likeComment(form);
+ this.findAndUpdateComment(voteRes);
+ }
+
+ async handlePostEdit(form: EditPost) {
+ const res = await HttpService.client.editPost(form);
+ this.findAndUpdatePost(res);
+ }
+
+ async handlePostVote(form: CreatePostLike) {
+ const voteRes = await HttpService.client.likePost(form);
+ this.findAndUpdatePost(voteRes);
+ }
+
+ async handleCommentReport(form: CreateCommentReport) {
+ const reportRes = await HttpService.client.createCommentReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handlePostReport(form: CreatePostReport) {
+ const reportRes = await HttpService.client.createPostReport(form);
+ if (reportRes.state == "success") {
+ toast(i18n.t("report_created"));
+ }
+ }
+
+ async handleLockPost(form: LockPost) {
+ const lockRes = await HttpService.client.lockPost(form);
+ this.findAndUpdatePost(lockRes);
+ }
+
+ async handleDistinguishComment(form: DistinguishComment) {
+ const distinguishRes = await HttpService.client.distinguishComment(form);
+ this.findAndUpdateComment(distinguishRes);
+ }
+
+ async handleAddAdmin(form: AddAdmin) {
+ const addAdminRes = await HttpService.client.addAdmin(form);
+
+ if (addAdminRes.state == "success") {
+ this.setState(s => ((s.siteRes.admins = addAdminRes.data.admins), s));
+ }
+ }
+
+ async handleTransferCommunity(form: TransferCommunity) {
+ await HttpService.client.transferCommunity(form);
+ toast(i18n.t("transfer_community"));
+ }
+
+ async handleCommentReplyRead(form: MarkCommentReplyAsRead) {
+ const readRes = await HttpService.client.markCommentReplyAsRead(form);
+ this.findAndUpdateCommentReply(readRes);
+ }
+
+ async handlePersonMentionRead(form: MarkPersonMentionAsRead) {
+ // TODO not sure what to do here. Maybe it is actually optional, because post doesn't need it.
+ await HttpService.client.markPersonMentionAsRead(form);
+ }
+
+ async handleBanFromCommunity(form: BanFromCommunity) {
+ const banRes = await HttpService.client.banFromCommunity(form);
+ this.updateBanFromCommunity(banRes);
+ }
+
+ async handleBanPerson(form: BanPerson) {
+ const banRes = await HttpService.client.banPerson(form);
+ this.updateBan(banRes);
+ }
+
+ updateBanFromCommunity(banRes: RequestState) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.postsRes.state == "success") {
+ s.postsRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(
+ c => (c.creator_banned_from_community = banRes.data.banned)
+ );
+ }
+ return s;
+ });
+ }
+ }
+
+ updateBan(banRes: RequestState) {
+ // Maybe not necessary
+ if (banRes.state == "success") {
+ this.setState(s => {
+ if (s.postsRes.state == "success") {
+ s.postsRes.data.posts
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ if (s.commentsRes.state == "success") {
+ s.commentsRes.data.comments
+ .filter(c => c.creator.id == banRes.data.person_view.person.id)
+ .forEach(c => (c.creator.banned = banRes.data.banned));
+ }
+ return s;
+ });
+ }
+ }
+
+ purgeItem(purgeRes: RequestState) {
+ if (purgeRes.state == "success") {
+ toast(i18n.t("purge_success"));
+ this.context.router.history.push(`/`);
+ }
+ }
+
+ findAndUpdateComment(res: RequestState) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editComment(
+ res.data.comment_view,
+ s.commentsRes.data.comments
+ );
+ s.finished.set(res.data.comment_view.comment.id, true);
+ }
+ return s;
+ });
+ }
+
+ createAndUpdateComments(res: RequestState) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments.unshift(res.data.comment_view);
+
+ // Set finished for the parent
+ s.finished.set(
+ getCommentParentId(res.data.comment_view.comment) ?? 0,
+ true
+ );
+ }
+ return s;
+ });
+ }
+
+ findAndUpdateCommentReply(res: RequestState) {
+ this.setState(s => {
+ if (s.commentsRes.state == "success" && res.state == "success") {
+ s.commentsRes.data.comments = editWith(
+ res.data.comment_reply_view,
+ s.commentsRes.data.comments
+ );
+ }
+ return s;
+ });
+ }
+
+ findAndUpdatePost(res: RequestState) {
+ this.setState(s => {
+ if (s.postsRes.state == "success" && res.state == "success") {
+ s.postsRes.data.posts = editPost(
+ res.data.post_view,
+ s.postsRes.data.posts
+ );
+ }
+ return s;
+ });
+ }
}
diff --git a/src/shared/components/home/instances.tsx b/src/shared/components/home/instances.tsx
index fd1ed617..bec472cf 100644
--- a/src/shared/components/home/instances.tsx
+++ b/src/shared/components/home/instances.tsx
@@ -3,68 +3,67 @@ import {
GetFederatedInstancesResponse,
GetSiteResponse,
Instance,
- UserOperation,
- wsJsonToRes,
- wsUserOp,
} from "lemmy-js-client";
-import { Subscription } from "rxjs";
import { i18n } from "../../i18next";
import { InitialFetchRequest } from "../../interfaces";
-import { WebSocketService } from "../../services";
-import {
- WithPromiseKeys,
- isBrowser,
- relTags,
- setIsoData,
- toast,
- wsClient,
- wsSubscribe,
-} from "../../utils";
+import { FirstLoadService } from "../../services/FirstLoadService";
+import { HttpService, RequestState } from "../../services/HttpService";
+import { RouteDataResponse, relTags, setIsoData } from "../../utils";
import { HtmlTags } from "../common/html-tags";
+import { Spinner } from "../common/icon";
-interface InstancesData {
+type InstancesData = RouteDataResponse<{
federatedInstancesResponse: GetFederatedInstancesResponse;
-}
+}>;
interface InstancesState {
+ instancesRes: RequestState;
siteRes: GetSiteResponse;
- instancesRes?: GetFederatedInstancesResponse;
- loading: boolean;
+ isIsomorphic: boolean;
}
export class Instances extends Component {
private isoData = setIsoData(this.context);
state: InstancesState = {
+ instancesRes: { state: "empty" },
siteRes: this.isoData.site_res,
- loading: true,
+ isIsomorphic: false,
};
- private subscription?: Subscription;
constructor(props: any, context: any) {
super(props, context);
- 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) {
+ if (FirstLoadService.isFirstLoad) {
this.state = {
...this.state,
instancesRes: this.isoData.routeData.federatedInstancesResponse,
- loading: false,
+ isIsomorphic: true,
};
- } else {
- WebSocketService.Instance.send(wsClient.getFederatedInstances({}));
}
}
- static fetchInitialData({
- client,
- }: InitialFetchRequest): WithPromiseKeys {
+ async componentDidMount() {
+ if (!this.state.isIsomorphic) {
+ await this.fetchInstances();
+ }
+ }
+
+ async fetchInstances() {
+ this.setState({
+ instancesRes: { state: "loading" },
+ });
+
+ this.setState({
+ instancesRes: await HttpService.client.getFederatedInstances({}),
+ });
+ }
+
+ static async fetchInitialData(
+ req: InitialFetchRequest
+ ): Promise {
return {
- federatedInstancesResponse: client.getFederatedInstances(
- {}
- ) as Promise,
+ federatedInstancesResponse: await req.client.getFederatedInstances({}),
};
}
@@ -72,43 +71,51 @@ export class Instances extends Component {
return `${i18n.t("instances")} - ${this.state.siteRes.site_view.site.name}`;
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
+ renderInstances() {
+ switch (this.state.instancesRes.state) {
+ case "loading":
+ return (
+
+
+
+ );
+ case "success": {
+ const instances = this.state.instancesRes.data.federated_instances;
+ return instances ? (
+
+
+
{i18n.t("linked_instances")}
+ {this.itemList(instances.linked)}
+
+ {instances.allowed && instances.allowed.length > 0 && (
+
+
{i18n.t("allowed_instances")}
+ {this.itemList(instances.allowed)}
+
+ )}
+ {instances.blocked && instances.blocked.length > 0 && (
+
+
{i18n.t("blocked_instances")}
+ {this.itemList(instances.blocked)}
+
+ )}
+
+ ) : (
+ <>>
+ );
+ }
}
}
render() {
- let federated_instances = this.state.instancesRes?.federated_instances;
- return federated_instances ? (
+ return (
-
-
-
{i18n.t("linked_instances")}
- {this.itemList(federated_instances.linked)}
-
- {federated_instances.allowed &&
- federated_instances.allowed.length > 0 && (
-
-
{i18n.t("allowed_instances")}
- {this.itemList(federated_instances.allowed)}
-
- )}
- {federated_instances.blocked &&
- federated_instances.blocked.length > 0 && (
-
-
{i18n.t("blocked_instances")}
- {this.itemList(federated_instances.blocked)}
-
- )}
-
+ {this.renderInstances()}
- ) : (
- <>>
);
}
@@ -142,17 +149,4 @@ export class Instances extends Component {
{i18n.t("none_found")}
);
}
- parseMessage(msg: any) {
- let op = wsUserOp(msg);
- console.log(msg);
- if (msg.error) {
- toast(i18n.t(msg.error), "danger");
- this.context.router.history.push("/");
- this.setState({ loading: false });
- return;
- } else if (op == UserOperation.GetFederatedInstances) {
- let data = wsJsonToRes(msg);
- this.setState({ loading: false, instancesRes: data });
- }
- }
}
diff --git a/src/shared/components/home/legal.tsx b/src/shared/components/home/legal.tsx
index d2c99532..15bea9b5 100644
--- a/src/shared/components/home/legal.tsx
+++ b/src/shared/components/home/legal.tsx
@@ -23,7 +23,7 @@ export class Legal extends Component {
}
render() {
- let legal = this.state.siteRes.site_view.local_site.legal_information;
+ const legal = this.state.siteRes.site_view.local_site.legal_information;
return (
;
form: {
username_or_email?: string;
password?: string;
totp_2fa_token?: string;
};
- loginLoading: boolean;
showTotp: boolean;
siteRes: GetSiteResponse;
}
export class Login extends Component {
private isoData = setIsoData(this.context);
- private subscription?: Subscription;
state: State = {
+ loginRes: { state: "empty" },
form: {},
- loginLoading: false,
showTotp: false,
siteRes: this.isoData.site_res,
};
constructor(props: any, context: any) {
super(props, context);
-
- this.parseMessage = this.parseMessage.bind(this);
- this.subscription = wsSubscribe(this.parseMessage);
-
- if (isBrowser()) {
- WebSocketService.Instance.send(wsClient.getCaptcha({}));
- }
}
componentDidMount() {
@@ -62,12 +39,6 @@ export class Login extends Component {
}
}
- componentWillUnmount() {
- if (isBrowser()) {
- this.subscription?.unsubscribe();
- }
- }
-
get documentTitle(): string {
return `${i18n.t("login")} - ${this.state.siteRes.site_view.site.name}`;
}
@@ -169,7 +140,11 @@ export class Login extends Component {