Albums almost done, group, chat moderation, photo, video page updates
This commit is contained in:
mgabdev 2020-12-21 13:25:05 -05:00
parent a2ffbadedb
commit ee91809e8d
45 changed files with 1013 additions and 509 deletions

View File

@ -7,12 +7,57 @@ module Admin
PER_PAGE = 100
def index
authorize :account, :index?
@followers = ChatMessage.where(from_account: @account).page(params[:page]).per(PER_PAGE)
authorize :chat_message, :index?
@chat_messages = ChatMessage.where(from_account: @account).page(params[:page]).per(PER_PAGE)
@form = Form::ChatMessageBatch.new
end
def show
authorize :chat_message, :index?
@chat_messages = @account.chat_messages.where(id: params[:id])
authorize @chat_messages.first, :show?
@form = Form::ChatMessageBatch.new
end
def create
authorize :chat_message, :update?
@form = Form::ChatMessageBatch.new(form_chat_message_batch_params.merge(current_account: current_account, action: action_from_button))
flash[:alert] = I18n.t('admin.statuses.failed_to_execute') unless @form.save
redirect_to admin_account_chat_messages_path(@account.id, current_params)
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.statuses.no_status_selected')
redirect_to admin_account_chat_messages_path(@account.id, current_params)
end
private
def form_chat_message_batch_params
params.require(:form_chat_message_batch).permit(:action, chat_message_ids: [])
end
def set_account
@account = Account.find(params[:account_id])
end
def current_params
page = (params[:page] || 1).to_i
{
media: params[:media],
page: page > 1 && page,
}.select { |_, value| value.present? }
end
def action_from_button
if params[:delete]
'delete'
end
end
end
end

View File

@ -23,7 +23,6 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
end
def hide_results?
# : todo : where tf is this?
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
end

View File

@ -19,13 +19,6 @@ class Api::V1::ChatConversationController < Api::BaseController
end
def create
# : todo :
# check if already created
# check if blocked
# check if chat blocked
# check if allow anyone to message then create with approved:true
# unique account id, participants
chat_conversation_account = find_or_create_conversation
render json: chat_conversation_account, each_serializer: REST::ChatConversationAccountSerializer
end
@ -80,26 +73,8 @@ class Api::V1::ChatConversationController < Api::BaseController
def find_or_create_conversation
chat = ChatConversationAccount.find_by(account: current_account, participant_account_ids: [@account.id.to_s])
return chat unless chat.nil?
chat_conversation = ChatConversation.create
my_chat = ChatConversationAccount.create!(
account: current_account,
participant_account_ids: [@account.id.to_s],
chat_conversation: chat_conversation,
is_approved: true
)
# : todo : if multiple ids
their_chat = ChatConversationAccount.create!(
account: @account,
participant_account_ids: [current_account.id.to_s],
chat_conversation: chat_conversation,
is_approved: false # default as request
)
my_chat = CreateChatConversationService.new.call(current_account, [@account])
return my_chat
end

View File

@ -34,10 +34,6 @@ class Api::V1::Groups::RemovedAccountsController < Api::BaseController
render_empty_success
end
def search
# : todo :
end
private
def set_group

View File

@ -3,7 +3,6 @@
class Api::V1::StatusesController < Api::BaseController
include Authorization
# : todo : disable all oauth everything
before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :update, :destroy]
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: [:create, :update, :destroy]
before_action :require_user!, except: [:show, :comments, :context, :card]

View File

@ -21,9 +21,6 @@ class Settings::GroupCategoriesController < Admin::BaseController
end
def destroy
# : todo :
# don't destroy if any groups have this category
@category.destroy!
log_action :destroy, @category
flash[:notice] = I18n.t('promotions.destroyed_msg')

View File

@ -26,6 +26,10 @@ class Settings::ProfilesController < Settings::BaseController
else
# : todo :
# only allowed to change username once per day
if params[:account][:username] && @account.username != params[:account][:username]
Redis.current.set("username_change:#{account.id}", true)
Redis.current.expire("username_change:#{account.id}", 24.huors.seconds)
end
if UpdateAccountService.new.call(@account, account_params)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')

View File

@ -101,7 +101,7 @@ module ApplicationHelper
end
def theme
# : todo :
# : todo : remove
return 'white'
end

View File

@ -7,10 +7,23 @@ export const ALBUMS_FETCH_REQUEST = 'ALBUMS_FETCH_REQUEST'
export const ALBUMS_FETCH_SUCCESS = 'ALBUMS_FETCH_SUCCESS'
export const ALBUMS_FETCH_FAIL = 'ALBUMS_FETCH_FAIL'
export const ALBUMS_CREATE_REQUEST = 'ALBUMS_CREATE_REQUEST'
export const ALBUMS_CREATE_SUCCESS = 'ALBUMS_CREATE_SUCCESS'
export const ALBUMS_CREATE_FAIL = 'ALBUMS_CREATE_FAIL'
export const ALBUMS_EXPAND_REQUEST = 'ALBUMS_EXPAND_REQUEST'
export const ALBUMS_EXPAND_SUCCESS = 'ALBUMS_EXPAND_SUCCESS'
export const ALBUMS_EXPAND_FAIL = 'ALBUMS_EXPAND_FAIL'
export const ALBUM_CREATE_REQUEST = 'ALBUM_CREATE_REQUEST'
export const ALBUM_CREATE_SUCCESS = 'ALBUM_CREATE_SUCCESS'
export const ALBUM_CREATE_FAIL = 'ALBUM_CREATE_FAIL'
export const ALBUM_REMOVE_REQUEST = 'ALBUM_REMOVE_REQUEST'
export const ALBUM_REMOVE_SUCCESS = 'ALBUM_REMOVE_SUCCESS'
export const ALBUM_REMOVE_FAIL = 'ALBUM_REMOVE_FAIL'
export const ALBUM_EDIT_REQUEST = 'ALBUM_EDIT_REQUEST'
export const ALBUM_EDIT_SUCCESS = 'ALBUM_EDIT_SUCCESS'
export const ALBUM_EDIT_FAIL = 'ALBUM_EDIT_FAIL'
export const ALBUM_UPDATE_MEDIA_REQUEST = 'ALBUM_UPDATE_MEDIA_REQUEST'
export const ALBUM_UPDATE_MEDIA_SUCCESS = 'ALBUM_UPDATE_MEDIA_SUCCESS'
export const ALBUM_UPDATE_MEDIA_FAIL = 'ALBUM_UPDATE_MEDIA_FAIL'
export const ALBUMS_REMOVE_REQUEST = 'ALBUMS_REMOVE_REQUEST'
export const ALBUMS_REMOVE_SUCCESS = 'ALBUMS_REMOVE_SUCCESS'
export const ALBUMS_REMOVE_FAIL = 'ALBUMS_REMOVE_FAIL'

View File

@ -1,8 +1,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { CX } from '../constants'
import { openModal } from '../actions/modal'
import {
CX,
MODAL_ALBUM_CREATE,
} from '../constants'
import Button from './button'
import Icon from './icon'
import Image from './image'
@ -10,19 +15,14 @@ import Text from './text'
class Album extends React.PureComponent {
handleOnClick = (e) => {
//
}
handleOnOpenAlbumCreation = () => {
handleOnOpenAlbumCreate = () => {
this.props.openAlbumCreate()
}
render() {
const {
album,
isAddable,
isDummy,
} = this.props
const title = isAddable ? 'New album' : 'Album title'
@ -31,29 +31,26 @@ class Album extends React.PureComponent {
return (
<div className={[_s.d, _s.minW162PX, _s.px5, _s.flex1].join(' ')}>
{
!isDummy &&
<Button
noClasses
className={[_s.d, _s.noUnderline, _s.noOutline, _s.bgTransparent].join(' ')}
to={to}
onClick={isAddable ? this.handleOnOpenAlbumCreation : undefined}
>
<div className={[_s.d, _s.w100PC, _s.mt5, _s.mb10].join(' ')}>
<div className={[_s.d, _s.w100PC, _s.pt100PC].join(' ')}>
<div className={[_s.d, _s.posAbs, _s.top0, _s.w100PC, _s.right0, _s.bottom0, _s.left0].join(' ')}>
<div className={[_s.d, _s.w100PC, _s.h100PC, _s.aiCenter, _s.jcCenter, _s.radiusSmall, _s.bgTertiary, _s.border1PX, _s.borderColorSecondary].join(' ')}>
{ isAddable && <Icon id='add' size='20px' /> }
</div>
<Button
noClasses
className={[_s.d, _s.noUnderline, _s.cursorPointer, _s.outlineNone, _s.bgTransparent].join(' ')}
to={to}
onClick={isAddable ? this.handleOnOpenAlbumCreate : undefined}
>
<div className={[_s.d, _s.w100PC, _s.mt5, _s.mb10].join(' ')}>
<div className={[_s.d, _s.w100PC, _s.pt100PC].join(' ')}>
<div className={[_s.d, _s.posAbs, _s.top0, _s.w100PC, _s.right0, _s.bottom0, _s.left0].join(' ')}>
<div className={[_s.d, _s.w100PC, _s.h100PC, _s.aiCenter, _s.jcCenter, _s.radiusSmall, _s.bgTertiary, _s.border1PX, _s.borderColorSecondary].join(' ')}>
{ isAddable && <Icon id='add' size='20px' /> }
</div>
</div>
</div>
<div className={[_s.d, _s.w100PC, _s.pt7, _s.mb15].join(' ')}>
<Text weight='bold'>{title}</Text>
{ !isAddable && <Text color='secondary' size='small' className={_s.mt5}>{subtitle}</Text> }
</div>
</Button>
}
</div>
<div className={[_s.d, _s.w100PC, _s.pt7, _s.mb15].join(' ')}>
<Text weight='bold'>{title}</Text>
{ !isAddable && <Text color='secondary' size='small' className={_s.mt5}>{subtitle}</Text> }
</div>
</Button>
</div>
)
}
@ -63,7 +60,12 @@ class Album extends React.PureComponent {
Album.propTypes = {
album: ImmutablePropTypes.map,
isAddable: PropTypes.bool,
isDummy: PropTypes.bool,
}
export default Album
const mapDispatchToProps = (dispatch) => ({
openAlbumCreate() {
dispatch(openModal(MODAL_ALBUM_CREATE))
}
})
export default connect(null, mapDispatchToProps)(Album)

View File

@ -28,7 +28,7 @@ class DeckColumnHeader extends React.PureComponent {
} = this.props
return (
<div data-sort-header className={[_s.d, _s.w100PC, _s.flexRow, _s.aiCenter, _s.h60PX, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary, _s.bgPrimary].join(' ')}>
<div data-sort-header className={[_s.d, _s.w100PC, _s.overflowHidden, _s.flexRow, _s.aiCenter, _s.h60PX, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary, _s.bgPrimary].join(' ')}>
<div data-sort-header className={[_s.d, _s.flexRow, _s.mr15, _s.cursorEWResize].join(' ')}>
<span className={[_s.d, _s.w1PX, _s.h24PX, _s.mr2, _s.bgSecondary].join(' ')} />
<span className={[_s.d, _s.w1PX, _s.h24PX, _s.mr2, _s.bgSecondary].join(' ')} />
@ -36,7 +36,7 @@ class DeckColumnHeader extends React.PureComponent {
</div>
{ !!icon && <Icon id={icon} className={[_s.cPrimary, _s.mr15].join(' ')} size='18px' /> }
<div className={[_s.d, _s.flexRow, _s.aiEnd].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.aiEnd, _s.flexShrink1, _s.overflowHidden, _s.textOverflowEllipsis2].join(' ')}>
{ !!title && <Text size='extraLarge' weight='medium'>{title}</Text> }
{ !!subtitle && <Text className={_s.ml5} color='secondary'>{subtitle}</Text> }
</div>

View File

@ -99,9 +99,6 @@ class GroupHeader extends ImmutablePureComponent {
})
}
// : todo :
// {group.get('archived') && <Icon id='lock' title={intl.formatMessage(messages.group_archived)} />}
return (
<div className={[_s.d, _s.z1, _s.w100PC, _s.mb15].join(' ')}>
<Responsive max={BREAKPOINT_EXTRA_SMALL}>

View File

@ -14,23 +14,41 @@ class MediaItem extends ImmutablePureComponent {
state = {
loaded: false,
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
visible: true,
}
componentDidMount() {
if (this.props.attachment.get('blurhash')) {
const { attachment } = this.props
if (!attachment) return
if (attachment.get('blurhash')) {
this._decode()
}
this.setState({
visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all',
})
}
componentDidUpdate(prevProps) {
if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) {
const { attachment } = this.props
const { prevAttachment } = prevProps
if (prevAttachment !== attachment) {
this._decode()
return
}
if (prevAttachment.get('blurhash') !== attachment.get('blurhash') && attachment.get('blurhash')) {
this._decode()
}
}
_decode() {
const hash = this.props.attachment.get('blurhash')
_decode = () => {
const { attachment } = this.props
if (!attachment) return
const hash = attachment.get('blurhash')
const pixels = decode(hash, 160, 160)
if (pixels && this.canvas) {
@ -41,7 +59,7 @@ class MediaItem extends ImmutablePureComponent {
}
}
setCanvasRef = c => {
setCanvasRef = (c) => {
this.canvas = c
}
@ -50,7 +68,10 @@ class MediaItem extends ImmutablePureComponent {
}
hoverToPlay() {
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1
const { attachment } = this.props
if (!attachment) return
return !autoPlayGif && ['gifv', 'video'].indexOf(attachment.get('type')) !== -1
}
render() {
@ -60,104 +81,152 @@ class MediaItem extends ImmutablePureComponent {
isSmall,
} = this.props
const { visible, loaded } = this.state
if (!attachment || !account) return null
const status = attachment.get('status')
const title = status.get('spoiler_text') || attachment.get('description')
const attachmentType = attachment.get('type')
const aspectRatio = attachment.getIn(['meta', 'aspect'])
const isVideo = attachmentType === 'video'
let badge = null
if (attachmentType === 'video') {
if (isVideo) {
const duration = attachment.getIn(['meta', 'duration'])
badge = (duration / 60).toFixed(2)
} else if (attachmentType === 'gifv') {
badge = 'GIF'
}
const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}`
const isSmallRatio = aspectRatio < 1
const isSquare = aspectRatio === 1
const containerClasses = CX({
d: 1,
posAbs: 1,
top0: 1,
h100PC: 1,
// w100PC: 1,
py2: !isSmall,
px2: !isSmall,
px5: 1,
flex1: !isSmallRatio && !isSquare,
minW198PX: !isVideo && !isSmallRatio && !isSquare,
minW232PX: isVideo && !isSmallRatio && !isSquare,
minW120PX: isSmallRatio,
minW162PX: isSquare,
})
const linkClasses = CX({
const paddedContainerClasses = CX({
d: 1,
w100PC: 1,
// h100PC: 1,
overflowHidden: 1,
border1PX: 1,
borderColorPrimary: 1,
h100PC: isSmallRatio || isSquare,
pt100PC: isSmallRatio || isSquare || !isVideo,
pt5625PC: isVideo && !isSmallRatio && !isSquare,
})
const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}`;
// : todo : fix dimensions to be like albums
return (
<div className={[_s.d, _s.pt25PC].join(' ')}>
<div className={containerClasses}>
<NavLink
to={statusUrl}
title={title}
className={linkClasses}
>
{
(!loaded || !visible) &&
<canvas
height='100%'
width='100%'
ref={this.setCanvasRef}
className={[_s.d, _s.w100PC, _s.h100PC, _s.z2].join(' ')}
/>
}
<div className={containerClasses}>
<NavLink
className={[_s.d, _s.noUnderline, _s.outlineNone, _s.bgTransparent, _s.flexGrow1].join(' ')}
to={statusUrl}
title={title}
>
<div className={[_s.d, _s.mt5, _s.mb10, _s.flexGrow1].join(' ')}>
<div className={paddedContainerClasses}>
<div className={[_s.d, _s.posAbs, _s.top0, _s.right0, _s.left0, _s.bottom0].join(' ')}>
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter, _s.radiusSmall, _s.overflowHidden].join(' ')}>
{
(!loaded || !visible) &&
<canvas
height='100%'
width='100%'
ref={this.setCanvasRef}
className={[_s.d, _s.w100PC, _s.h100PC, _s.z2].join(' ')}
/>
}
{
visible &&
<Image
height='100%'
width=''
src={attachment.get('preview_url')}
alt={attachment.get('description')}
title={attachment.get('description')}
onLoad={this.handleImageLoad}
className={_s.z1}
/>
}
{
visible &&
<Image
height='100%'
width=''
src={attachment.get('preview_url')}
alt={attachment.get('description')}
title={attachment.get('description')}
onLoad={this.handleImageLoad}
className={_s.z1}
/>
}
<div className={[_s.d, _s.aiCenter, _s.jcCenter, _s.h100PC, _s.w100PC, _s.z3, _s.posAbs].join(' ')}>
{
!visible &&
<Icon
id='hidden'
size='22px'
className={[_s.cWhite].join('')}
/>
}
{
!!badge &&
<div className={[_s.d, _s.posAbs, _s.radiusSmall, _s.bgBlackOpaque, _s.px5, _s.py5, _s.mr5, _s.mt5, _s.mb5, _s.bottom0, _s.right0].join(' ')}>
<Text size='extraSmall' color='white'>
{badge}
</Text>
{
(!visible || !!badge) &&
<div className={[_s.d, _s.aiCenter, _s.jcCenter, _s.h100PC, _s.w100PC, _s.z3, _s.posAbs].join(' ')}>
{
!visible &&
<Icon
id='hidden'
size='22px'
className={[_s.cWhite].join('')}
/>
}
{
!!badge &&
<div className={[_s.d, _s.posAbs, _s.radiusSmall, _s.bgBlackOpaque, _s.px5, _s.py5, _s.mr5, _s.mt5, _s.mb5, _s.bottom0, _s.right0].join(' ')}>
<Text size='extraSmall' color='white'>
{badge}
</Text>
</div>
}
</div>
}
</div>
}
</div>
</div>
</NavLink>
</div>
</div>
</NavLink>
</div>
)
// return (
// <div className={[_s.d, _s.pt25PC].join(' ')}>
// <div className={containerClasses}>
// <NavLink
// to={statusUrl}
// title={title}
// className={linkClasses}
// >
// {
// (!loaded || !visible) &&
// <canvas
// height='100%'
// width='100%'
// ref={this.setCanvasRef}
// className={[_s.d, _s.w100PC, _s.h100PC, _s.z2].join(' ')}
// />
// }
// {
// visible &&
// <Image
// height='100%'
// width=''
// src={attachment.get('preview_url')}
// alt={attachment.get('description')}
// title={attachment.get('description')}
// onLoad={this.handleImageLoad}
// className={_s.z1}
// />
// }
// </NavLink>
// </div>
// </div>
// )
}
}
MediaItem.propTypes = {
isDummy: PropTypes.bool.isRequired,
account: ImmutablePropTypes.map.isRequired,
attachment: ImmutablePropTypes.map.isRequired,
isSmall: PropTypes.bool,

View File

@ -1 +1,29 @@
// : todo :
import React from 'react'
import PropTypes from 'prop-types'
import { defineMessages, injectIntl } from 'react-intl'
import ModalLayout from './modal_layout'
import AlbumCreate from '../../features/album_create'
class AlbumCreateModal extends React.PureComponent {
render() {
const { onClose } = this.props
return (
<ModalLayout
title='Create Album'
width={500}
onClose={onClose}
>
<AlbumCreate isModal />
</ModalLayout>
)
}
}
AlbumCreateModal.propTypes = {
onClose: PropTypes.func.isRequired,
}
export default AlbumCreateModal

View File

@ -1,29 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { defineMessages, injectIntl } from 'react-intl'
import ModalLayout from './modal_layout'
import BookmarkCollectionEdit from '../../features/bookmark_collection_edit'
class BookmarkCollectionEditModal extends React.PureComponent {
render() {
const { onClose } = this.props
return (
<ModalLayout
title='Edit Bookmark Collection'
width={500}
onClose={onClose}
>
<BookmarkCollectionEdit isModal />
</ModalLayout>
)
}
}
BookmarkCollectionEditModal.propTypes = {
onClose: PropTypes.func.isRequired,
}
export default BookmarkCollectionEditModal

View File

@ -39,12 +39,6 @@ class MediaGalleryPanel extends ImmutablePureComponent {
}
}
// componentWillReceiveProps(nextProps) {
// if (nextProps.accountId && nextProps.accountId !== this.props.accountId) {
// this.props.dispatch(expandAccountMediaTimeline(nextProps.accountId, { limit: 8 }))
// }
// }
render() {
const {
account,

View File

@ -81,8 +81,6 @@ class StatusOptionsPopover extends ImmutablePureComponent {
handleGroupRemoveAccount = () => {
const { status } = this.props
// : todo : check
this.props.onGroupRemoveAccount(status.getIn(['group', 'id']), status.getIn(['account', 'id']))
}

View File

@ -65,6 +65,19 @@ class DeckSidebar extends ImmutablePureComponent {
<div className={[_s.d].join(' ')}>
<Button
to='/'
isText
title='Go home'
aria-label='Go home'
color='none'
backgroundColor='none'
className={[_s.d, _s.jcCenter, _s.noSelect, _s.noUnderline, _s.mt15, _s.mb15, _s.cursorPointer, _s.px10, _s.mr5].join(' ')}
icon='back'
iconSize='20px'
iconClassName={_s.fillNavigationBrand}
/>
<h1 className={[_s.d].join(' ')}>
<Button
to='/'

View File

@ -51,7 +51,6 @@ export const POPOVER_VIDEO_STATS = 'VIDEO_STATS'
export const MODAL_ALBUM_CREATE = 'ALBUM_CREATE'
export const MODAL_BLOCK_ACCOUNT = 'BLOCK_ACCOUNT'
export const MODAL_BOOKMARK_COLLECTION_CREATE = 'BOOKMARK_COLLECTION_CREATE'
export const MODAL_BOOKMARK_COLLECTION_EDIT = 'BOOKMARK_COLLECTION_EDIT'
export const MODAL_BOOST = 'BOOST'
export const MODAL_CHAT_CONVERSATION_CREATE = 'CHAT_CONVERSATION_CREATE'
export const MODAL_CHAT_CONVERSATION_DELETE = 'CHAT_CONVERSATION_DELETE'

View File

@ -3,76 +3,60 @@ import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { injectIntl, defineMessages } from 'react-intl'
import { expandAccountMediaTimeline } from '../actions/timelines'
import { getAccountGallery } from '../selectors'
import { me } from '../initial_state'
import {
fetchAccountAlbums,
expandAccountAlbums,
} from '../actions/albums'
import ColumnIndicator from '../components/column_indicator'
import Heading from '../components/heading'
import TabBar from '../components/tab_bar'
import MediaItem from '../components/media_item'
import LoadMore from '../components/load_more'
import Block from '../components/block'
import Image from '../components/image'
import Album from '../components/album'
import Dummy from '../components/dummy'
import MediaGalleryPlaceholder from '../components/placeholder/media_gallery_placeholder'
class AccountAlbums extends ImmutablePureComponent {
// componentDidMount() {
// const { accountId, mediaType } = this.props
componentDidMount() {
const { accountId, mediaType } = this.props
// if (accountId && accountId !== -1) {
// this.props.dispatch(expandAccountMediaTimeline(accountId, { mediaType }))
// }
// }
if (accountId && accountId !== -1) {
this.props.onFetchAccountAlbums(accountId)
}
}
// componentWillReceiveProps(nextProps) {
// if (
// (nextProps.accountId && nextProps.accountId !== this.props.accountId) ||
// (nextProps.accountId && nextProps.mediaType !== this.props.mediaType)
// ) {
// this.props.dispatch(expandAccountMediaTimeline(nextProps.accountId, {
// mediaType: nextProps.mediaType,
// }))
// }
// }
componentWillReceiveProps(nextProps) {
if (nextProps.accountId && nextProps.accountId !== this.props.accountId) {
this.props.onFetchAccountAlbums(nextProps.accountId)
}
}
// handleScrollToBottom = () => {
// if (this.props.hasMore) {
// this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined)
// }
// }
handleLoadMore = () => {
const { accountId, hasMore } = this.props
if (accountId && accountId !== -1 && hasMore) {
this.props.onExpandAccountAlbums(accountId)
}
}
// handleScroll = (e) => {
// const { scrollTop, scrollHeight, clientHeight } = e.target
// const offset = scrollHeight - scrollTop - clientHeight
// if (150 > offset && !this.props.isLoading) {
// this.handleScrollToBottom()
// }
// }
// handleLoadMore = (maxId) => {
// if (this.props.accountId && this.props.accountId !== -1) {
// this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, {
// maxId,
// mediaType: this.props.mediaType,
// }))
// }
// }
// handleLoadOlder = (e) => {
// e.preventDefault()
// this.handleScrollToBottom()
// }
handleLoadOlder = (e) => {
e.preventDefault()
this.handleLoadMore()
}
render() {
const {
account,
isMe,
albums,
account,
accountId,
hasMore,
isLoading,
} = this.props
if (!account) return null
const hasAlbums = !!albums ? albums.size > 0 : false
return (
<Block>
@ -95,103 +79,68 @@ class AccountAlbums extends ImmutablePureComponent {
<div className={[_s.d, _s.w100PC, _s.flexRow, _s.flexWrap, _s.px10, _s.mb15, _s.pb10].join(' ')}>
{ isMe && <Album isAddable /> }
<Album />
<Album />
<Album />
<Album />
<Album />
<Album isDummy />
<Album isDummy />
<Album isDummy />
<Album isDummy />
<Album isDummy />
<Album isDummy />
{
hasAlbums &&
albums.map((albums, i) => (
<Album
key={album.get('id')}
album={album}
account={account}
/>
))
}
{
Array.apply(null, { length: 8}).map((_, i) => (
<Dummy className={[_s.d, _s.minW162PX, _s.px5, _s.flex1].join(' ')} />
))
}
{
!isLoading && !hasAlbums && me !== accountId &&
<ColumnIndicator type='error' message='No albums exist' />
}
</div>
{
hasMore && !(isLoading && !hasAlbums) &&
<LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />
}
</Block>
)
// const {
// attachments,
// isLoading,
// hasMore,
// intl,
// account,
// } = this.props
// return (
// <Block>
// <div
// role='feed'
// onScroll={this.handleScroll}
// className={[_s.d, _s.flexRow, _s.flexWrap, _s.py5, _s.px5].join(' ')}
// >
// {
// attachments.map((attachment, i) => (
// <MediaItem
// key={attachment.get('id')}
// attachment={attachment}
// account={account}
// />
// ))
// }
// {
// isLoading && attachments.size === 0 &&
// <div className={[_s.d, _s.w100PC].join(' ')}>
// <MediaGalleryPlaceholder />
// </div>
// }
// {
// !isLoading && attachments.size === 0 &&
// <ColumnIndicator type='error' message={intl.formatMessage(messages.none)} />
// }
// </div>
// {
// hasMore && !(isLoading && attachments.size === 0) &&
// <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />
// }
// </Block>
// )
}
}
const messages = defineMessages({
none: { id: 'account_gallery.none', defaultMessage: 'No media to show.' },
})
const mapStateToProps = (state, { account, mediaType }) => {
const accountId = !!account ? account.get('id') : -1
return {
accountId,
attachments: getAccountGallery(state, accountId, mediaType),
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
albums: state.getIn(['album_lists', accountId, 'items']),
isLoading: state.getIn(['album_lists', accountId, 'isLoading'], false),
hasMore: state.getIn(['album_lists', accountId, 'hasMore'], false),
}
}
const mapDispatchToProps = (dispatch) => ({
onFetchAccountAlbums(accountId) {
},
onExpandAccountAlbums(accountId) {
},
})
AccountAlbums.propTypes = {
dispatch: PropTypes.func.isRequired,
account: ImmutablePropTypes.map,
accountId: PropTypes.string,
attachments: ImmutablePropTypes.list.isRequired,
albums: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
intl: PropTypes.object.isRequired,
mediaType: PropTypes.oneOf([
'photo',
'video',
]),
}
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(AccountAlbums))
export default connect(mapStateToProps, mapDispatchToProps)(AccountAlbums)

View File

@ -0,0 +1,172 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { injectIntl, defineMessages } from 'react-intl'
import { expandAccountMediaTimeline } from '../actions/timelines'
import { getAccountGallery } from '../selectors'
import ColumnIndicator from '../components/column_indicator'
import MediaItem from '../components/media_item'
import Heading from '../components/heading'
import TabBar from '../components/tab_bar'
import LoadMore from '../components/load_more'
import Block from '../components/block'
import Dummy from '../components/dummy'
import MediaGalleryPlaceholder from '../components/placeholder/media_gallery_placeholder'
class AccountPhotoGallery extends ImmutablePureComponent {
componentDidMount() {
const { accountId } = this.props
if (accountId && accountId !== -1) {
this.props.dispatch(expandAccountMediaTimeline(accountId, { mediaType: 'photo' }))
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.accountId && nextProps.accountId !== this.props.accountId) {
this.props.dispatch(expandAccountMediaTimeline(nextProps.accountId, {
mediaType: 'photo',
}))
}
}
handleScrollToBottom = () => {
if (this.props.hasMore) {
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined)
}
}
handleScroll = (e) => {
const { scrollTop, scrollHeight, clientHeight } = e.target
const offset = scrollHeight - scrollTop - clientHeight
if (150 > offset && !this.props.isLoading) {
this.handleScrollToBottom()
}
}
handleLoadMore = (maxId) => {
if (this.props.accountId && this.props.accountId !== -1) {
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, {
maxId,
mediaType: 'photo',
}))
}
}
handleLoadOlder = (e) => {
e.preventDefault()
this.handleScrollToBottom()
}
render() {
const {
attachments,
isLoading,
hasMore,
intl,
account,
} = this.props
if (!account) return null
const hasAttachments = !!attachments ? attachments.size > 0 : false
console.log("account, isLoading, attachments:", account, isLoading, attachments, hasAttachments)
return (
<Block>
<div className={[_s.d, _s.px10, _s.py10].join(' ')}>
<div className={[_s.d, _s.px5, _s.py5, _s.mb10].join(' ')}>
<Heading size='h2'>Photos</Heading>
</div>
<TabBar tabs={[
{
title: 'All Photos',
to: `/${account.get('username')}/photos`,
},
{
title: 'Albums',
isActive: true,
to: `/${account.get('username')}/albums`,
},
]}/>
</div>
<div
role='feed'
onScroll={this.handleScroll}
className={[_s.d, _s.w100PC, _s.flexRow, _s.flexWrap, _s.px10, _s.mb15, _s.pb10].join(' ')}
>
{
hasAttachments &&
<React.Fragment>
{
attachments.map((attachment, i) => (
<MediaItem
key={attachment.get('id')}
attachment={attachment}
account={account}
/>
))
}
{
Array.apply(null, { length: 8}).map((_, i) => (
<Dummy className={[_s.d, _s.minW198PX, _s.px5, _s.flex1].join(' ')} />
))
}
</React.Fragment>
}
{
isLoading && !hasAttachments &&
<div className={[_s.d, _s.w100PC].join(' ')}>
<MediaGalleryPlaceholder />
</div>
}
{
!isLoading && !hasAttachments &&
<ColumnIndicator type='error' message={intl.formatMessage(messages.none)} />
}
</div>
{
hasMore && !(isLoading && !hasAttachments) &&
<LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />
}
</Block>
)
}
}
const messages = defineMessages({
none: { id: 'account_gallery.none', defaultMessage: 'No media to show.' },
})
const mapStateToProps = (state, { account }) => {
const accountId = !!account ? account.get('id') : -1
return {
accountId,
attachments: getAccountGallery(state, accountId, 'photo'),
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
}
}
AccountPhotoGallery.propTypes = {
dispatch: PropTypes.func.isRequired,
account: ImmutablePropTypes.map,
accountId: PropTypes.string,
attachments: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
intl: PropTypes.object.isRequired,
}
export default injectIntl(connect(mapStateToProps)(AccountPhotoGallery))

View File

@ -8,27 +8,27 @@ import { expandAccountMediaTimeline } from '../actions/timelines'
import { getAccountGallery } from '../selectors'
import ColumnIndicator from '../components/column_indicator'
import MediaItem from '../components/media_item'
import Dummy from '../components/dummy'
import LoadMore from '../components/load_more'
import Block from '../components/block'
import Heading from '../components/heading'
import TabBar from '../components/tab_bar'
import MediaGalleryPlaceholder from '../components/placeholder/media_gallery_placeholder'
class AccountGallery extends ImmutablePureComponent {
class AccountVideoGallery extends ImmutablePureComponent {
componentDidMount() {
const { accountId, mediaType } = this.props
const { accountId } = this.props
if (accountId && accountId !== -1) {
this.props.dispatch(expandAccountMediaTimeline(accountId, { mediaType }))
this.props.dispatch(expandAccountMediaTimeline(accountId, { mediaType: 'video' }))
}
}
componentWillReceiveProps(nextProps) {
if (
(nextProps.accountId && nextProps.accountId !== this.props.accountId) ||
(nextProps.accountId && nextProps.mediaType !== this.props.mediaType)
) {
if (nextProps.accountId && nextProps.accountId !== this.props.accountId) {
this.props.dispatch(expandAccountMediaTimeline(nextProps.accountId, {
mediaType: nextProps.mediaType,
mediaType: 'video',
}))
}
}
@ -52,7 +52,7 @@ class AccountGallery extends ImmutablePureComponent {
if (this.props.accountId && this.props.accountId !== -1) {
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, {
maxId,
mediaType: this.props.mediaType,
mediaType: 'video',
}))
}
}
@ -73,39 +73,63 @@ class AccountGallery extends ImmutablePureComponent {
if (!account) return null
const hasAttachments = !!attachments ? attachments.size > 0 : false
return (
<Block>
<div className={[_s.d, _s.px10, _s.py10].join(' ')}>
<div className={[_s.d, _s.px5, _s.py5, _s.mb10].join(' ')}>
<Heading size='h2'>Videos</Heading>
</div>
<TabBar tabs={[
{
title: 'All Videos',
to: `/${account.get('username')}/videos`,
},
]}/>
</div>
<div
role='feed'
onScroll={this.handleScroll}
className={[_s.d, _s.flexRow, _s.flexWrap, _s.py5, _s.px5].join(' ')}
className={[_s.d, _s.w100PC, _s.flexRow, _s.flexWrap, _s.px10, _s.mb15, _s.pb10].join(' ')}
>
{
attachments.map((attachment, i) => (
<MediaItem
key={attachment.get('id')}
attachment={attachment}
account={account}
/>
))
hasAttachments &&
<React.Fragment>
{
attachments.map((attachment, i) => (
<MediaItem
key={attachment.get('id')}
attachment={attachment}
account={account}
/>
))
}
{
Array.apply(null, { length: 8 }).map((_, i) => (
<Dummy className={[_s.d, _s.minW232PX, _s.px5, _s.flex1].join(' ')} />
))
}
</React.Fragment>
}
{
isLoading && attachments.size === 0 &&
isLoading && !hasAttachments &&
<div className={[_s.d, _s.w100PC].join(' ')}>
<MediaGalleryPlaceholder />
</div>
}
{
!isLoading && attachments.size === 0 &&
!isLoading && !hasAttachments &&
<ColumnIndicator type='error' message={intl.formatMessage(messages.none)} />
}
</div>
{
hasMore && !(isLoading && attachments.size === 0) &&
hasMore && !(isLoading && !hasAttachments) &&
<LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />
}
</Block>
@ -118,18 +142,18 @@ const messages = defineMessages({
none: { id: 'account_gallery.none', defaultMessage: 'No media to show.' },
})
const mapStateToProps = (state, { account, mediaType }) => {
const mapStateToProps = (state, { account }) => {
const accountId = !!account ? account.get('id') : -1
return {
accountId,
attachments: getAccountGallery(state, accountId, mediaType),
attachments: getAccountGallery(state, accountId, 'video'),
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
}
}
AccountGallery.propTypes = {
AccountVideoGallery.propTypes = {
dispatch: PropTypes.func.isRequired,
account: ImmutablePropTypes.map,
accountId: PropTypes.string,
@ -137,14 +161,6 @@ AccountGallery.propTypes = {
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
intl: PropTypes.object.isRequired,
mediaType: PropTypes.oneOf([
'photo',
'video',
]),
}
AccountGallery.defaultProps = {
mediaType: 'both'
}
export default injectIntl(connect(mapStateToProps)(AccountGallery))
export default injectIntl(connect(mapStateToProps)(AccountVideoGallery))

View File

@ -1 +1,67 @@
// : todo :
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import { createAlbum } from '../actions/albums'
import { closeModal } from '../actions/modal'
import Button from '../components/button'
import Input from '../components/input'
import Form from '../components/form'
import Text from '../components/text'
class AlbumCreate extends React.PureComponent {
state = {
value: '',
}
onChange = (value) => {
this.setState({ value })
}
handleOnSubmit = () => {
this.props.onSubmit(this.state.value)
}
render() {
const { value } = this.state
const isDisabled = !value
return (
<Form>
<Input
title='Title'
placeholder='Album title'
value={value}
onChange={this.onChange}
/>
<Button
isDisabled={isDisabled}
onClick={this.handleOnSubmit}
className={[_s.mt10].join(' ')}
>
<Text color='inherit' align='center'>
Create
</Text>
</Button>
</Form>
)
}
}
const mapDispatchToProps = (dispatch, { isModal }) => ({
onSubmit(title) {
if (isModal) dispatch(closeModal())
dispatch(createBookmarkCollection(title))
},
})
AlbumCreate.propTypes = {
onSubmit: PropTypes.func.isRequired,
isModal: PropTypes.bool,
}
export default connect(null, mapDispatchToProps)(AlbumCreate)

View File

@ -1,100 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import {
updateBookmarkCollection,
removeBookmarkCollection,
} from '../actions/bookmarks'
import { closeModal } from '../actions/modal'
import Button from '../components/button'
import Input from '../components/input'
import Form from '../components/form'
import Text from '../components/text'
class BookmarkCollectionEdit extends React.PureComponent {
state = {
value: '',
}
componentDidMount() {
if (!this.props.bookmarkCollection) {
this.props.onFetchBookmarkCollection(this.props.bookmarkCollectionId)
}
}
onChange = (value) => {
this.setState({ value })
}
handleOnSubmit = () => {
this.props.onSubmit(this.state.value)
}
handleOnRemove = () => {
this.props.onRemove()
}
render() {
const { value } = this.state
const isDisabled = !value
return (
<Form>
<Input
title='Title'
placeholder='Bookmark collection title'
value={value}
onChange={this.onChange}
/>
<Button
isDisabled={isDisabled}
onClick={this.handleOnSubmit}
className={[_s.mt10].join(' ')}
>
<Text color='inherit' align='center'>
Update
</Text>
</Button>
<Button
backgroundColor='danger'
color='white'
onClick={this.handleOnRemove}
className={[_s.mt10].join(' ')}
>
<Text color='inherit' align='center'>
Update
</Text>
</Button>
</Form>
)
}
}
const mapStateToProps = (state, { bookmarkCollectionId }) => ({
bookmarkCollection: state.getIn(['bookmark_collections', bookmarkCollectionId]),
})
const mapDispatchToProps = (dispatch, { isModal, bookmarkCollectionId }) => ({
onSubmit(title) {
if (isModal) dispatch(closeModal())
dispatch(updateBookmarkCollection(title))
},
onRemove() {
if (isModal) dispatch(closeModal())
dispatch(removeBookmarkCollection(bookmarkCollectionId))
},
})
BookmarkCollectionEdit.propTypes = {
onSubmit: PropTypes.func.isRequired,
onRemove: PropTypes.func.isRequired,
isModal: PropTypes.bool,
}
export default connect(mapStateToProps, mapDispatchToProps)(BookmarkCollectionEdit)

View File

@ -10,6 +10,7 @@ import {
import { me, meUsername} from '../initial_state'
import {
GAB_DECK_MAX_ITEMS,
URL_GAB_PRO,
MODAL_PRO_UPGRADE,
} from '../constants'
import {
@ -22,6 +23,7 @@ import { openModal } from '../actions/modal'
import WrappedBundle from './ui/util/wrapped_bundle'
import DeckColumn from '../components/deck_column'
import Text from '../components/text'
import Button from '../components/button'
import {
AccountTimeline,
Compose,
@ -186,6 +188,15 @@ class Deck extends React.PureComponent {
const isEmpty = gabDeckOrder.size === 0
const title = (
<span className={[_s.d, _s.flexRow, _s.jcCenter, _s.aiCenter].join(' ')}>
<span className={[_s.d, _s.mr2].join(' ')}>
Gab Deck for Gab
</span>
<span className={[_s.bgPro, _s.cBlack, _s.radiusSmall, _s.px5, _s.py5].join(' ')}>PRO</span>
</span>
)
return (
<SortableContainer
axis='x'
@ -199,16 +210,29 @@ class Deck extends React.PureComponent {
<DeckColumn title='Compose' icon='pencil' noButtons>
<WrappedBundle component={Compose} />
</DeckColumn>
{ /** : todo : */
{
!isPro &&
<DeckColumn title='Gab Deck for GabPRO' icon='pro' noButtons>
<DeckColumn title={title} icon='pro' noButtons>
<div className={[_s.d, _s.px15, _s.py15].join(' ')}>
<Text>
Gab Deck for GabPRO. Some text about what it does and some buttons on going pro to use it.
GabDeck is a unique way to customize your Gab experience. Upgrade to GabPRO to unlock the GabDeck.
</Text>
<div className={[_s.mt15, _s.d, _s.flexRow].join(' ')}>
<Button href={URL_GAB_PRO}>
<Text color='inherit' className={_s.px10}>
Upgrade to GabPRO
</Text>
</Button>
</div>
</div>
</DeckColumn>
}
<DeckColumn title='Explore' icon='explore' noButtons>
<WrappedBundle component={ExploreTimeline} />
</DeckColumn>
<DeckColumn title='News' icon='news' noButtons>
<WrappedBundle component={News} componentParams={{ isSmall: true }} />
</DeckColumn>
<DeckColumn />
</React.Fragment>
}

View File

@ -97,7 +97,6 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
}
getCurrentChatMessageIndex = (id) => {
// : todo :
return this.props.chatMessageIds.indexOf(id)
}

View File

@ -53,7 +53,8 @@ import DeckPage from '../../pages/deck_page'
import {
About,
AccountAlbums,
AccountGallery,
AccountPhotoGallery,
AccountVideoGallery,
AccountTimeline,
AccountCommentsTimeline,
AlbumCreate,
@ -277,17 +278,18 @@ class SwitchingArea extends React.PureComponent {
<WrappedRoute path='/:username/followers' page={ProfilePage} component={Followers} content={children} />
<WrappedRoute path='/:username/following' page={ProfilePage} component={Following} content={children} />
<WrappedRoute path='/:username/photos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'photo' }} />
<WrappedRoute path='/:username/videos' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true, mediaType: 'video' }} />
<WrappedRoute path='/:username/albums' page={ProfilePage} component={AccountAlbums} content={children} componentParams={{ noSidebar: true, mediaType: 'photo' }} />
<WrappedRoute path='/:username/photos' exact page={ProfilePage} component={AccountPhotoGallery} content={children} componentParams={{ noSidebar: true }} />
{ /* <WrappedRoute path='/:username/albums/:albumId' page={ProfilePage} component={AccountGallery} content={children} componentParams={{ noSidebar: true }} /> */ }
<WrappedRoute path='/:username/videos' exact page={ProfilePage} component={AccountVideoGallery} content={children} componentParams={{ noSidebar: true }} />
<WrappedRoute path='/:username/albums' exact page={ProfilePage} component={AccountAlbums} content={children} componentParams={{ noSidebar: true }} />
<WrappedRoute path='/:username/album_create' page={ModalPage} component={AlbumCreate} content={children} componentParams={{ title: 'Create Album', page: 'create-album' }} />
<WrappedRoute path='/:username/album_edit/:albumId' page={ModalPage} component={AlbumCreate} content={children} componentParams={{ title: 'Create Album', page: 'edit-album' }} />
{ /* <WrappedRoute path='/:username/albums/create' exact page={ModalPage} component={AlbumCreate} content={children} componentParams={{ title: 'Create Album', page: 'create-album' }} /> */ }
{ /* <WrappedRoute path='/:username/albums/:albumId/edit' page={ModalPage} component={AlbumCreate} content={children} componentParams={{ title: 'Edit Album', page: 'edit-album' }} /> */ }
<WrappedRoute path='/:username/likes' page={ProfilePage} component={LikedStatuses} content={children} />
<WrappedRoute path='/:username/bookmark_collections/create' page={ModalPage} component={BookmarkCollectionCreate} content={children} componentParams={{ title: 'Create Bookmark Collection', page: 'create-bookmark-collection' }} />
<WrappedRoute path='/:username/bookmark_collections/:bookmarkCollectionId' page={ProfilePage} component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/:username/bookmark_collections/:bookmarkCollectionId/edit' page={ModalPage} component={BookmarkCollectionEdit} content={children} componentParams={{ title: 'Edit Bookmark Collection', page: 'edit-bookmark-collection' }} />
<WrappedRoute path='/:username/bookmark_collections/:bookmarkCollectionId/edit' page={ModalPage} component={BookmarkCollectionCreate} content={children} componentParams={{ title: 'Edit Bookmark Collection', page: 'edit-bookmark-collection' }} />
<WrappedRoute path='/:username/bookmark_collections' page={ProfilePage} component={BookmarkCollections} content={children} />
<WrappedRoute path='/:username/posts/:statusId' publicRoute exact page={BasicPage} component={StatusFeature} content={children} componentParams={{ title: 'Status', page: 'status' }} />

View File

@ -3,7 +3,8 @@ export function AboutSidebar() { return import(/* webpackChunkName: "components/
export function AccountTimeline() { return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline') }
export function AccountCommentsTimeline() { return import(/* webpackChunkName: "features/account_comments_timeline" */'../../account_comments_timeline') }
export function AccountAlbums() { return import(/* webpackChunkName: "features/account_albums" */'../../account_albums') }
export function AccountGallery() { return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery') }
export function AccountPhotoGallery() { return import(/* webpackChunkName: "features/account_photo_gallery" */'../../account_photo_gallery') }
export function AccountVideoGallery() { return import(/* webpackChunkName: "features/account_video_gallery" */'../../account_video_gallery') }
export function AlbumCreate() { return import(/* webpackChunkName: "features/album_create" */'../../album_create') }
export function AlbumCreateModal() { return import(/* webpackChunkName: "components/album_create_modal" */'../../../components/modal/album_create_modal') }
export function Assets() { return import(/* webpackChunkName: "features/about/assets" */'../../about/assets') }
@ -12,8 +13,6 @@ export function BlockedAccounts() { return import(/* webpackChunkName: "features
export function BookmarkCollections() { return import(/* webpackChunkName: "features/bookmark_collections" */'../../bookmark_collections') }
export function BookmarkCollectionCreate() { return import(/* webpackChunkName: "features/bookmark_collection_create" */'../../bookmark_collection_create') }
export function BookmarkCollectionCreateModal() { return import(/* webpackChunkName: "components/bookmark_collection_create_modal" */'../../../components/modal/bookmark_collection_create_modal') }
export function BookmarkCollectionEdit() { return import(/* webpackChunkName: "features/bookmark_collection_edit" */'../../bookmark_collection_edit') }
export function BookmarkCollectionEditModal() { return import(/* webpackChunkName: "components/bookmark_collection_edit_modal" */'../../../components/modal/bookmark_collection_edit_modal') }
export function BookmarkedStatuses() { return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses') }
export function BoostModal() { return import(/* webpackChunkName: "components/boost_modal" */'../../../components/modal/boost_modal') }
export function CaliforniaConsumerProtection() { return import(/* webpackChunkName: "features/california_consumer_protection" */'../../about/california_consumer_protection') }

View File

@ -34,7 +34,10 @@ class GroupPage extends ImmutablePureComponent {
const isMember = !!relationships ? relationships.get('member') : false
const unavailable = isPrivate && !isMember
if (!!group) {
if (group.get('archived')) return null
}
return (
<GroupLayout
title={'Group'}

View File

@ -0,0 +1,59 @@
import {
Map as ImmutableMap,
List as ImmutableList,
} from 'immutable'
import { me } from '../initial_state'
import {
ALBUMS_FETCH_REQUEST,
ALBUMS_FETCH_SUCCESS,
ALBUMS_FETCH_FAIL,
ALBUMS_EXPAND_REQUEST,
ALBUMS_EXPAND_SUCCESS,
ALBUMS_EXPAND_FAIL,
} from '../actions/albums'
const initialState = ImmutableMap({})
const setListFailed = (state, id) => {
return state.setIn([id], ImmutableMap({
next: null,
items: ImmutableList(),
isLoading: false,
}))
}
const normalizeList = (state, id, albums, next) => {
return state.setIn([id], ImmutableMap({
next,
items: ImmutableList(albums.map(item => item.id)),
isLoading: false,
}))
}
const appendToList = (state, id, albums, next) => {
return state.updateIn([id], (map) => {
return map
.set('next', next)
.set('isLoading', false)
.update('items', (list) => {
return list.concat(albums.map(item => item.id))
})
})
}
export default function album_lists(state = initialState, action) {
switch(action.type) {
case ALBUMS_FETCH_REQUEST:
case ALBUMS_EXPAND_REQUEST:
return state.setIn([action.accountId, 'isLoading'], true)
case ALBUMS_FETCH_SUCCESS:
return normalizeList(state, action.accountId, action.albums, action.next)
case ALBUMS_EXPAND_SUCCESS:
return appendToList(state, action.accountId, action.albums, action.next)
case ALBUMS_FETCH_FAIL:
case ALBUMS_EXPAND_FAIL:
return setListFailed(state, action.accountId)
default:
return state;
}
};

View File

@ -3,6 +3,7 @@ import { loadingBarReducer } from 'react-redux-loading-bar'
import accounts from './accounts'
import accounts_counters from './accounts_counters'
import albums from './albums'
import album_lists from './album_lists'
import bookmark_collections from './bookmark_collections'
import chats from './chats'
import chat_conversation_lists from './chat_conversation_lists'
@ -55,6 +56,8 @@ import user_lists from './user_lists'
const reducers = {
accounts,
accounts_counters,
// albums,
// album_lists,
bookmark_collections,
chats,
chat_conversation_lists,

View File

@ -603,6 +603,8 @@ pre {
.maxW212PX { max-width: 212px; }
.minW330PX { min-width: 330px; }
.minW232PX { min-width: 232px; }
.minW198PX { min-width: 192px; }
.minW162PX { min-width: 162px; }
.minW120PX { min-width: 120px; }
.minW84PX { min-width: 84px; }

View File

@ -17,7 +17,6 @@
# is_muted :boolean default(FALSE), not null
#
# : todo : expires
# : todo : max per account
class ChatConversationAccount < ApplicationRecord
include Paginable

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
class Form::ChatMessageBatch
include ActiveModel::Model
include AccountableConcern
attr_accessor :chat_message_ids, :action, :current_account
def save
case action
when 'delete'
delete_chat_messages
end
end
private
def delete_chat_messages
ChatMessage.where(id: chat_message_ids).reorder(nil).find_each do |chat_message|
DeleteChatMessageWorker.perform_async(chat_message.id)
log_action :destroy, chat_message
end
true
end
end

View File

@ -12,11 +12,10 @@
#
class Shortcut < ApplicationRecord
# : todo : enum 1,2, etc.
# enum shortcut_type: {
# account: 'account',
# group: 'group'
# }
SHORTCUT_TYPE_MAP = {
account: 'account',
group: 'group',
}.freeze
belongs_to :account

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
class ChatMessagePolicy < ApplicationPolicy
def initialize(current_account, record, preloaded_relations = {})
super(current_account, record)
@preloaded_relations = preloaded_relations
end
def index?
staff?
end
def show?
if requires_mention?
owned? || mention_exists?
elsif private?
owned? || following_author? || mention_exists?
else
current_account.nil? || !author_blocking?
end
end
def reblog?
!requires_mention? && (!private? || owned?) && show? && !blocking_author?
end
def favourite?
show? && !blocking_author?
end
def destroy?
staff? || owned?
end
alias unreblog? destroy?
def update?
staff? || owned?
end
private
def requires_mention?
record.limited_visibility?
end
def owned?
author.id == current_account&.id
end
def private?
record.private_visibility?
end
def mention_exists?
return false if current_account.nil?
if record.mentions.loaded?
record.mentions.any? { |mention| mention.account_id == current_account.id }
else
record.mentions.where(account: current_account).exists?
end
end
def blocking_author?
return false if current_account.nil?
@preloaded_relations[:blocking] ? @preloaded_relations[:blocking][author.id] : current_account.blocking?(author)
end
def author_blocking?
return false if current_account.nil?
@preloaded_relations[:blocked_by] ? @preloaded_relations[:blocked_by][author.id] : author.blocking?(current_account)
end
def following_author?
return false if current_account.nil?
@preloaded_relations[:following] ? @preloaded_relations[:following][author.id] : current_account.following?(author)
end
def author
record.account
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
class CreateChatConversationService < BaseService
def call(current_account, other_accounts)
@current_account = current_account
@other_accounts = other_accounts
return nil if @other_accounts.nil? || @current_account.nil?
# check if already created
chat = ChatConversationAccount.find_by(account: current_account, participant_account_ids: account_ids_as_array)
return chat unless chat.nil?
# : todo :
# check if allow anyone to message then create with approved:true
# unique account id, participants
chat_conversation = ChatConversation.create
my_chat = ChatConversationAccount.create!(
account: current_account,
participant_account_ids: [@account.id.to_s],
chat_conversation: chat_conversation,
is_approved: true
)
# : todo : if multiple ids
if @account.id != current_account.id
their_chat = ChatConversationAccount.create!(
account: @account,
participant_account_ids: [current_account.id.to_s],
chat_conversation: chat_conversation,
is_approved: false # : todo : check if allow all else default as request
)
end
end
def account_ids_as_array
@other_accounts.map { |account| account.id.to_s }
end
end

View File

@ -78,16 +78,12 @@ class FetchLinkCardService < BaseService
end
def parse_urls
# : todo :
if @status.local?
urls = @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize }
return urls.reject { |uri| bad_url?(uri) }.first
else
html = Nokogiri::HTML(@status.text)
links = html.css('a')
urls = links.map { |a| Addressable::URI.parse(a['href']).normalize unless skip_link?(a) }.compact
return nil
end
urls.reject { |uri| bad_url?(uri) }.first
end
def bad_url?(uri)

View File

@ -2,7 +2,7 @@
%td
= admin_account_link_to(account)
- if account.is_flagged_as_spam
%span SPAM
%span{ :style => "display:block;margin:5px 0 0 20px;font-size:12px;background-color:#781600;border-radius:6px;color:#fff;width:40px;line-height:22px;font-weight:600;padding:2px 0 0 6px;" } SPAM
%td
- if account.user_current_sign_in_ip
%samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip

View File

@ -123,6 +123,14 @@
%time.formatted{ datetime: @account.created_at.iso8601, title: l(@account.created_at) }= l @account.created_at
%td
%tr
%th Is flagged as spam
%td
- if @account.is_flagged_as_spam?
%span YES
- else
%span no
%tr
%th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip

View File

@ -0,0 +1,16 @@
.batch-table__row
%label.batch-table__row__select.batch-checkbox
= f.check_box :chat_message_ids, { multiple: true, include_hidden: false }, chat_message.id
.batch-table__row__content
%div{:style=>"display:flex;flex-direction:column;"}
%span= chat_message.text
%div{:style=> "display:block;color:#555;"}
%span Created:
%span= chat_message.created_at
- if chat_message.expires_at
%div{:style=> "display:block;color:#555;"}
%span Expires:
%span= chat_message.expires_at
%div{:style=> "display:block;color:#555;"}
%span In conversation:
%span= chat_message.chat_conversation_id

View File

@ -1,28 +1,29 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.followers.title', acct: @account.acct)
%span Chat Messages
\-
= "@#{@account.acct}"
.filters
.filter-subset
%strong= t('admin.accounts.location.title')
%ul
%li= link_to t('admin.accounts.location.local'), admin_account_followers_path(@account.id), class: 'selected'
.back-link{ style: 'flex: 1 1 auto; text-align: right' }
= link_to admin_account_path(@account.id) do
= fa_icon 'chevron-left fw'
= t('admin.followers.back_to_account')
%span Back to account
%hr.spacer/
.table-wrapper
%table.table
%thead
%tr
%th= t('admin.accounts.username')
%th= t('admin.accounts.role')
%th= t('admin.accounts.most_recent_ip')
%th= t('admin.accounts.most_recent_activity')
%th
%tbody
= render partial: 'admin/accounts/account', collection: @followers
= form_for(@form, url: admin_account_chat_messages_path(@account.id)) do |f|
= hidden_field_tag :page, params[:page]
= paginate @followers
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= f.button safe_join([fa_icon('trash'), 'Delete']), name: :delete, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
.batch-table__body
= render partial: 'admin/chat_messages/chat_message', collection: @chat_messages, locals: { f: f }
= paginate @chat_messages

View File

@ -4,11 +4,14 @@
%td= group.member_count
%td
- if group.is_featured?
= t('admin.groups.featured')
%span Y
%td
- if not group.is_featured?
= table_link_to 'power-off', t('admin.groups.enable_featured'), enable_featured_admin_group_path(group, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
- else
= table_link_to 'power-off', t('admin.groups.disable_featured'), disable_featured_admin_group_path(group, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
%td
= table_link_to 'times', t('admin.groups.delete'), admin_group_path(group, page: params[:page], **@filter_params), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
= table_link_to '', 'Edit', admin_group_path(group)
-# %td
-# - if not group.is_featured?
-# = table_link_to 'power-off', t('admin.groups.enable_featured'), enable_featured_admin_group_path(group, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
-# - else
-# = table_link_to 'power-off', t('admin.groups.disable_featured'), disable_featured_admin_group_path(group, page: params[:page], **@filter_params), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }
-# %td
-# = table_link_to 'times', t('admin.groups.delete'), admin_group_path(group, page: params[:page], **@filter_params), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }

View File

@ -17,7 +17,7 @@
%th= t('admin.groups.id')
%th= t('admin.groups.title')
%th= t('admin.groups.member_count')
%th
%th Featured?
%th
%th
%tbody

View File

@ -8,28 +8,60 @@
-# delete
-# view accounts
-# view removed accounts
-# number of accounts
-# number of removed accounts
-# number of posts
.card.h-card{:style => "height:300px"}
.card__img
= image_tag @group.cover_image.url, alt: '', :style => "height:300px"
= image_tag full_asset_url(@group.cover_image.url), alt: '', :style => "height:300px"
.dashboard__counters{ style: 'margin-top: 10px' }
%div
%div
.dashboard__counters__num= number_with_delimiter 0 #@account.statuses_count
.dashboard__counters__num= number_with_delimiter Status.where(group:@group).count
.dashboard__counters__label Status Count
%div
%div
.dashboard__counters__num= number_to_human_size 0 #@account.media_attachments.sum('file_file_size')
.dashboard__counters__num= number_with_delimiter @group.accounts.count
.dashboard__counters__label Member Count
%div
%div
.dashboard__counters__num= number_with_delimiter 0 #@account.local_followers_count
.dashboard__counters__num= number_with_delimiter @group.removed_accounts.count
.dashboard__counters__label Removed Members Count
%div
%div
.dashboard__counters__num= number_with_delimiter 0 #@account.reports.count
.dashboard__counters__label Member Requests Count
.dashboard__counters__num= number_with_delimiter @group.join_requests.count
.dashboard__counters__label Member Requests Count
= simple_form_for(@group, url: admin_group_path(@group.id), html: { method: :put }) do |f|
= render 'shared/error_messages', object: @group
.fields-group
= f.input :title, wrapper: :with_label, label: 'Title'
.fields-group
= f.input :description, wrapper: :with_label, label: 'Description'
.fields-group
= f.input :slug, wrapper: :with_label, label: 'Slug'
.fields-group
= f.input :tags, wrapper: :with_label, label: 'Tags'
.fields-group
= f.input :is_nsfw, as: :boolean, wrapper: :with_label, label: 'Is NSFW?'
.fields-group
= f.input :is_featured, as: :boolean, wrapper: :with_label, label: 'Is Featured?'
.fields-group
= f.input :is_private, as: :boolean, wrapper: :with_label, label: 'Is Private?'
.fields-group
= f.input :is_visible, as: :boolean, wrapper: :with_label, label: 'Is Visible?'
.actions
= f.button :button, t('generic.save_changes'), type: :submit
-# : todo : delete
-# : todo : list admins
-# : todo : list mods
-# : todo : make ME admin