This commit is contained in:
mgabdev 2020-12-15 19:31:30 -05:00
parent de0c977950
commit 75d52c841e
129 changed files with 2559 additions and 910 deletions

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Admin
class ChatConversationsController < BaseController
before_action :set_account
PER_PAGE = 20
def index
authorize :account, :index?
@chatConversationAccounts = ChatConversationAccount.where(account: @account).page(params[:page]).per(PER_PAGE)
end
def set_account
@account = Account.find(params[:account_id])
end
end
end

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Admin
class ChatMessagesController < BaseController
before_action :set_account
PER_PAGE = 100
def index
authorize :account, :index?
@followers = ChatMessage.where(from_account: @account).page(params[:page]).per(PER_PAGE)
end
def set_account
@account = Account.find(params[:account_id])
end
end
end

@ -5,6 +5,9 @@ module Admin
class DashboardController < BaseController
def index
@users_count = User.count
@statuses_count = Status.count
@pro_accounts_count = Account.where(is_pro: true).count
@donor_accounts_count = Account.where(is_donor: true).count
@registrations_week = Redis.current.get("activity:accounts:local:#{current_week}") || 0
@logins_week = Redis.current.pfcount("activity:logins:#{current_week}")
@interactions_week = Redis.current.get("activity:interactions:#{current_week}") || 0

@ -1,67 +1,75 @@
# frozen_string_literal: true
module Admin
class GroupsController < BaseController
before_action :set_group, except: [:index]
before_action :set_filter_params
def index
authorize :group, :index?
@groups = filtered_groups.page(params[:page])
end
def destroy
authorize @group, :destroy?
@group.destroy!
log_action :destroy, @group
flash[:notice] = I18n.t('admin.groups.destroyed_msg')
redirect_to admin_groups_path(page: params[:page], **@filter_params)
end
def enable_featured
authorize @group, :update?
@group.is_featured = true
@group.save!
log_action :update, @group
flash[:notice] = I18n.t('admin.groups.updated_msg')
redirect_to admin_groups_path(page: params[:page], **@filter_params)
end
def disable_featured
authorize @group, :update?
@group.is_featured = false
@group.save!
log_action :update, @group
flash[:notice] = I18n.t('admin.groups.updated_msg')
redirect_to admin_groups_path(page: params[:page], **@filter_params)
end
private
def set_group
@group = Group.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def resource_params
params.require(:group).permit(:is_featured, :is_nsfw)
end
def filtered_groups
query = Group.order('is_featured DESC, member_count DESC')
if params[:title]
query = query.where("LOWER(title) LIKE LOWER(?)", "%#{params[:title]}%")
end
return query
end
def filter_params
params.permit(:sort,)
end
class GroupsController < BaseController
before_action :set_group, except: [:index]
before_action :set_filter_params
def index
authorize :group, :index?
@groups = filtered_groups.page(params[:page])
end
def show
authorize :group, :index?
end
def update
#
end
def destroy
authorize @group, :destroy?
@group.destroy!
log_action :destroy, @group
flash[:notice] = I18n.t('admin.groups.destroyed_msg')
redirect_to admin_groups_path(page: params[:page], **@filter_params)
end
def enable_featured
authorize @group, :update?
@group.is_featured = true
@group.save!
log_action :update, @group
flash[:notice] = I18n.t('admin.groups.updated_msg')
redirect_to admin_groups_path(page: params[:page], **@filter_params)
end
def disable_featured
authorize @group, :update?
@group.is_featured = false
@group.save!
log_action :update, @group
flash[:notice] = I18n.t('admin.groups.updated_msg')
redirect_to admin_groups_path(page: params[:page], **@filter_params)
end
private
def set_group
@group = Group.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def resource_params
params.require(:group).permit(:is_featured, :is_nsfw)
end
def filtered_groups
query = Group.order('is_featured DESC, member_count DESC')
if params[:title]
query = query.where("LOWER(title) LIKE LOWER(?)", "%#{params[:title]}%")
end
return query
end
def filter_params
params.permit(:sort,)
end
end
end

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Admin
class JoinedGroupsController < BaseController
before_action :set_account
PER_PAGE = 25
def index
authorize :account, :index?
@groups = @account.groups.page(params[:page]).per(PER_PAGE)
end
def set_account
@account = Account.find(params[:account_id])
end
end
end

@ -12,6 +12,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
def update
@account = current_account
# : todo : add link blocking check for bio
UpdateAccountService.new.call(@account, account_params, raise_error: true)
UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params
render json: @account, serializer: REST::CredentialAccountSerializer

@ -0,0 +1,44 @@
# frozen_string_literal: true
class Api::V1::AlbumsController < Api::BaseController
before_action :require_user!
before_action :set_albums, only: :index
before_action :set_album, only: [:show, :update, :destroy]
def index
render json: @albums, each_serializer: REST::AlbumSerializer
end
def create
@album = "" #current_account.custom_filters.create!(resource_params)
render json: @album, serializer: REST::AlbumSerializer
end
def show
render json: @album, serializer: REST::AlbumSerializer
end
def update
@album.update!(resource_params)
render json: @album, serializer: REST::AlbumSerializer
end
def destroy
@album.destroy!
render_empty_success
end
private
def set_albums
@albums = "" #current_account.custom_filters
end
def set_album
@album = "" # current_account.custom_filters.find(params[:id])
end
def resource_params
params.permit(:title, :description, :visibility)
end
end

@ -0,0 +1,44 @@
# frozen_string_literal: true
class Api::V1::BookmarkCollectionsController < Api::BaseController
before_action :require_user!
before_action :set_bookmark_collections, only: :index
before_action :set_bookmark_collection, only: [:show, :update, :destroy]
def index
render json: @bookmark_collections, each_serializer: REST::BookmarkCollectionSerializer
end
def create
@bookmark_collection = "" #current_account.custom_filters.create!(resource_params)
render json: @bookmark_collection, serializer: REST::BookmarkCollectionSerializer
end
def show
render json: @bookmark_collection, serializer: REST::BookmarkCollectionSerializer
end
def update
@bookmark_collection.update!(resource_params)
render json: @bookmark_collection, serializer: REST::BookmarkCollectionSerializer
end
def destroy
@bookmark_collection.destroy!
render_empty_success
end
private
def set_bookmark_collections
@bookmark_collections = "" #current_account.custom_filters
end
def set_bookmark_collection
@bookmark_collection = "" # current_account.custom_filters.find(params[:id])
end
def resource_params
params.permit(:title)
end
end

@ -35,6 +35,15 @@ class Api::V1::ChatConversationAccountsController < Api::BaseController
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
end
def set_expiration_policy
if current_user.account.is_pro
#
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
else
render json: { error: 'You need to be a GabPRO member to access this' }, status: 422
end
end
private
def set_account

@ -6,7 +6,7 @@ class Api::V1::ChatConversationController < Api::BaseController
before_action :require_user!
before_action :set_account, only: :create
before_action :set_chat_conversation, only: [:show, :mark_chat_conversation_approved, :mark_chat_conversation_hidden, :mark_chat_conversation_unread]
before_action :set_chat_conversation, only: [:show, :mark_chat_conversation_approved, :mark_chat_conversation_hidden, :mark_chat_conversation_read]
def show
render json: {}, each_serializer: REST::ChatConversationAccountSerializer
@ -23,8 +23,8 @@ class Api::V1::ChatConversationController < Api::BaseController
render json: chat_conversation_account, each_serializer: REST::ChatConversationAccountSerializer
end
def mark_chat_conversation_unread
@chat_conversation_account.update!(unread_count: 1)
def mark_chat_conversation_read
@chat_conversation_account.update!(unread_count: 0)
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
end
@ -34,8 +34,13 @@ class Api::V1::ChatConversationController < Api::BaseController
end
def mark_chat_conversation_approved
@chat_conversation_account.update!(is_approved: true)
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
approved_conversation_count = ChatConversationAccount.where(account: @account, is_hidden: false, is_approved: true).count
if approved_conversation_count >= ChatConversationAccount::PER_ACCOUNT_APPROVED_LIMIT
render json: { error: true, message: "You have #{approved_conversation_count} active chat conversations. The limit is #{ChatConversationAccount::PER_ACCOUNT_APPROVED_LIMIT}. Delete some conversations first before approving any more requests." }
else
@chat_conversation_account.update!(is_approved: true)
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
end
end
private

@ -11,24 +11,16 @@ class Api::V1::ChatConversations::MessagesController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @chats.empty? }
def show
puts "tilly chat_message_conversations - 1: " + @chats.count.inspect
render json: @chats, each_serializer: REST::ChatMessageSerializer
end
def destroy_all
puts "tilly destry all chat"
# : todo :
# check if is pro
# @chat = ChatMessage.where(from_account: current_user.account).find(params[:id])
puts "tilly @chat: " + @chat.inspect
# : todo :
# make sure last_chat_message_id in chat_account_conversation gets set to last
# @chat.destroy!
# render json: @chat, serializer: REST::ChatMessageSerializer
if current_user.account.is_pro
@chat_conversation_account = PurgeChatMessagesService.new.call(current_user.account, @chat_conversation)
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
else
render json: { error: 'You need to be a GabPRO member to access this' }, status: 422
end
end
private

@ -5,50 +5,20 @@ class Api::V1::ChatMessagesController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:chats' }
before_action :require_user!
before_action :set_chat_conversation, only: :create
before_action :set_chat_conversation_recipients, only: :create
def create
@chat = ChatMessage.create!(
from_account: current_account,
chat_conversation: @chat_conversation,
text: ActionController::Base.helpers.strip_tags(params[:text])
)
# : todo :
# check if blocked
# update unread_count++ if offline
@chat_conversation_recipients.each do |account|
payload = InlineRenderer.render(@chat, account, :chat_message)
Redis.current.publish("chat_messages:#{account.id}", Oj.dump(event: :notification, payload: payload))
end
@chat_conversation = ChatConversation.find(chat_params[:chat_conversation_id])
@chat = PostChatMessageService.new.call(current_user.account, text: chat_params[:text], chat_conversation: @chat_conversation)
render json: @chat, serializer: REST::ChatMessageSerializer
end
def destroy
@chat = ChatMessage.where(from_account: current_user.account).find(params[:id])
# : todo :
# make sure last_chat_message_id in chat_account_conversation gets set to last
@chat.destroy!
@chat = DeleteChatMessageService.new.call(current_user.account, params[:id])
render json: @chat, serializer: REST::ChatMessageSerializer
end
private
def set_chat_conversation
@chat_conversation = ChatConversation.find(params[:chat_conversation_id])
end
def set_chat_conversation_recipients
account_conversation = ChatConversationAccount.where(account: current_user.account, chat_conversation: @chat_conversation).first
@chat_conversation_recipients = Account.where(id: account_conversation.participant_account_ids)
end
def chat_params
params.permit(:text, :chat_conversation_id)
end

@ -46,7 +46,7 @@ class Api::V1::GroupsController < Api::BaseController
@groups = []
if !@groupCategory.nil?
@groups = Group.where(is_archived: false, group_categories: @groupCategory).all
@groups = Group.where(is_archived: false, group_categories: @groupCategory).order('member_count DESC').all
end
render json: @groups, each_serializer: REST::GroupSerializer
@ -59,7 +59,7 @@ class Api::V1::GroupsController < Api::BaseController
@groups = []
if !params[:tag].empty?
@groups = Group.where(is_archived: false).where("array_to_string(tags, '||') ILIKE :tag", tag: "%#{params[:tag]}%").all
@groups = Group.where(is_archived: false).where("array_to_string(tags, '||') ILIKE :tag", tag: "%#{params[:tag]}%").order('member_count DESC').all
end
render json: @groups, each_serializer: REST::GroupSerializer

@ -62,6 +62,6 @@ class Api::V1::Timelines::HomeController < Api::BaseController
end
def regeneration_in_progress?
Redis.current.exists("account:#{current_account.id}:regeneration")
Redis.current.exists?("account:#{current_account.id}:regeneration")
end
end

@ -48,7 +48,25 @@ class EmptyController < ActionController::Base
nil
end
protected
def cache_collection(raw, klass)
return raw unless klass.respond_to?(:with_includes)
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
unless uncached_ids.empty?
uncached = klass.where(id: uncached_ids).with_includes.each_with_object({}) { |item, h| h[item.id] = item }
uncached.each_value do |item|
Rails.cache.write(item, item)
end
end
raw.map { |item| cached_keys_with_value[item.id] || uncached[item.id] }.compact
end
def limit_param(default_limit)
return default_limit unless params[:limit]

@ -3,7 +3,7 @@
class ManifestsController < EmptyController
def show
render json: InstancePresenter.new, serializer: ManifestSerializer
render json:{} # InstancePresenter.new, serializer: ManifestSerializer
end
end

@ -20,7 +20,13 @@ class Settings::ProfilesController < Settings::BaseController
if @account.is_verified && params[:account][:display_name] && @account.display_name != params[:account][:display_name]
flash[:alert] = 'Unable to change Display name for verified account'
redirect_to settings_profile_path
elsif !@account.is_pro && params[:account][:username] && @account.username != params[:account][:username]
flash[:alert] = 'Unable to change username for your account. You are not GabPRO'
redirect_to settings_profile_path
else
# : todo :
# only allowed to change username once per day
if UpdateAccountService.new.call(@account, account_params)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
else
@ -33,7 +39,7 @@ class Settings::ProfilesController < Settings::BaseController
private
def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
params.require(:account).permit(:display_name, :username, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
end
def set_account

@ -46,11 +46,11 @@ class Settings::PromotionsController < Admin::BaseController
@promotion = Promotion.find(params[:id])
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def set_filter_params
@filter_params = filter_params.to_hash.symbolize_keys
end
def resource_params
params.require(:promotion).permit(:expires_at, :status_id, :timeline_id, :position)
end
def resource_params
params.require(:promotion).permit(:expires_at, :status_id, :timeline_id, :position)
end
end

@ -0,0 +1,10 @@
class Settings::TrendingHashtagsController < Admin::BaseController
def index
@trending_hashtags = Redis.current.get("admin_trending_hashtags") || ''
end
def create
Redis.current.set("admin_trending_hashtags", params[:trending_hashtags])
redirect_to settings_trending_hashtags_path
end
end

@ -1,10 +1,10 @@
class Settings::Verifications::ModerationController < Admin::BaseController
def index
@verification_requests = AccountVerificationRequest.all
@verification_requests = AccountVerificationRequest.order('created_at DESC').all
end
def approve
verification_request = AccountVerificationRequest.find params[:id]
verification_request = AccountVerificationRequest.find(params[:id])
# Mark user as verified
account = verification_request.account
@ -22,6 +22,8 @@ class Settings::Verifications::ModerationController < Admin::BaseController
end
def reject
@verification_requests = AccountVerificationRequest.find params[:id]
verification_request = AccountVerificationRequest.find(params[:id])
verification_request.destroy()
redirect_to settings_verifications_moderation_url, notice: I18n.t('verifications.moderation.rejected_msg')
end
end

@ -38,12 +38,6 @@ module StatusesHelper
content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
end
def account_badge(account, all: false)
if account.bot?
content_tag(:div, content_tag(:div, t('accounts.roles.bot'), class: 'account-role bot'), class: 'roles')
end
end
def link_to_more(url)
link_to t('statuses.show_more'), url, class: 'load-more load-gap'
end

@ -0,0 +1 @@
//

@ -10,6 +10,20 @@ export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_RE
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS'
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL'
//
export const BOOKMARK_COLLECTIONS_FETCH_REQUEST = 'BOOKMARK_COLLECTIONS_FETCH_REQUEST'
export const BOOKMARK_COLLECTIONS_FETCH_SUCCESS = 'BOOKMARK_COLLECTIONS_FETCH_SUCCESS'
export const BOOKMARK_COLLECTIONS_FETCH_FAIL = 'BOOKMARK_COLLECTIONS_FETCH_FAIL'
export const BOOKMARK_COLLECTIONS_CREATE_REQUEST = 'BOOKMARK_COLLECTIONS_CREATE_REQUEST'
export const BOOKMARK_COLLECTIONS_CREATE_SUCCESS = 'BOOKMARK_COLLECTIONS_CREATE_SUCCESS'
export const BOOKMARK_COLLECTIONS_CREATE_FAIL = 'BOOKMARK_COLLECTIONS_CREATE_FAIL'
export const BOOKMARK_COLLECTIONS_REMOVE_REQUEST = 'BOOKMARK_COLLECTIONS_REMOVE_REQUEST'
export const BOOKMARK_COLLECTIONS_REMOVE_SUCCESS = 'BOOKMARK_COLLECTIONS_REMOVE_SUCCESS'
export const BOOKMARK_COLLECTIONS_REMOVE_FAIL = 'BOOKMARK_COLLECTIONS_REMOVE_FAIL'
/**
*
*/
@ -22,11 +36,11 @@ export const fetchBookmarkedStatuses = () => (dispatch, getState) => {
dispatch(fetchBookmarkedStatusesRequest())
api(getState).get('/api/v1/bookmarks').then(response => {
api(getState).get('/api/v1/bookmarks').then((response) => {
const next = getLinks(response).refs.find(link => link.rel === 'next')
dispatch(importFetchedStatuses(response.data))
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null))
}).catch(error => {
}).catch((error) => {
dispatch(fetchBookmarkedStatusesFail(error))
})
}
@ -61,11 +75,11 @@ export const expandBookmarkedStatuses = () => (dispatch, getState) => {
dispatch(expandBookmarkedStatusesRequest())
api(getState).get(url).then(response => {
api(getState).get(url).then((response) => {
const next = getLinks(response).refs.find(link => link.rel === 'next')
dispatch(importFetchedStatuses(response.data))
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null))
}).catch(error => {
}).catch((error) => {
dispatch(expandBookmarkedStatusesFail(error))
})
}
@ -85,3 +99,95 @@ const expandBookmarkedStatusesFail = (error) => ({
showToast: true,
error,
})
/**
*
*/
export const fetchBookmarkCollections = () => (dispatch, getState) => {
if (!me) return
if (getState().getIn(['bookmark_collections', 'isLoading'])) return
dispatch(fetchBookmarkCollectionsRequest())
api(getState).get('/api/v1/bookmark_collections').then((response) => {
dispatch(fetchBookmarkCollectionsSuccess(response.data))
}).catch((error) => {
dispatch(fetchBookmarkCollectionsFail(error))
})
}
const fetchBookmarkCollectionsRequest = () => ({
type: BOOKMARK_COLLECTIONS_FETCH_REQUEST,
})
const fetchBookmarkCollectionsSuccess = (bookmarkCollections) => ({
type: BOOKMARK_COLLECTIONS_FETCH_SUCCESS,
bookmarkCollections,
})
const fetchBookmarkCollectionsFail = (error) => ({
type: BOOKMARK_COLLECTIONS_FETCH_FAIL,
showToast: true,
error,
})
/**
*
*/
export const createBookmarkCollection = (title) => (dispatch, getState) => {
if (!me || !title) return
dispatch(createBookmarkCollectionRequest())
api(getState).post('/api/v1/bookmark_collections', { title }).then((response) => {
dispatch(createBookmarkCollectionSuccess(response.data))
}).catch((error) => {
dispatch(createBookmarkCollectionFail(error))
})
}
const createBookmarkCollectionRequest = () => ({
type: BOOKMARK_COLLECTIONS_CREATE_REQUEST,
})
const createBookmarkCollectionSuccess = (bookmarkCollection) => ({
type: BOOKMARK_COLLECTIONS_CREATE_SUCCESS,
bookmarkCollection,
})
const createBookmarkCollectionFail = (error) => ({
type: BOOKMARK_COLLECTIONS_CREATE_FAIL,
showToast: true,
error,
})
/**
*
*/
export const removeBookmarkCollection = (bookmarkCollectionId) => (dispatch, getState) => {
if (!me || !bookmarkCollectionId) return
dispatch(removeBookmarkCollectionRequest(bookmarkCollectionId))
api(getState).delete(`/api/v1/bookmark_collection/${bookmarkCollectionId}`).then((response) => {
dispatch(removeBookmarkCollectionSuccess(response.data))
}).catch((error) => {
dispatch(removeBookmarkCollectionFail(error))
})
}
const removeBookmarkCollectionRequest = (bookmarkCollectionId) => ({
type: BOOKMARK_COLLECTIONS_CREATE_REQUEST,
bookmarkCollectionId,
})
const removeBookmarkCollectionSuccess = () => ({
type: BOOKMARK_COLLECTIONS_CREATE_SUCCESS,
})
const removeBookmarkCollectionFail = (error) => ({
type: BOOKMARK_COLLECTIONS_CREATE_FAIL,
showToast: true,
error,
})

@ -46,6 +46,7 @@ export const IS_CHAT_MESSENGER_MUTED_SUCCESS = 'IS_CHAT_MESSENGER_MUTED_SUCCESS'
*
*/
export const blockChatMessenger = (accountId) => (dispatch, getState) => {
console.log("blockChatMessenger:", accountId)
if (!me || !accountId) return
dispatch(blockChatMessengerRequest(accountId))

@ -44,10 +44,10 @@ export const clearChatMessageConversation = (chatConversationId) => (dispatch) =
/**
*
*/
export const scrollBottomChatMessageConversation = (chatConversationId, top) => ({
export const scrollBottomChatMessageConversation = (chatConversationId, bottom) => ({
type: CHAT_CONVERSATION_MESSAGES_SCROLL_BOTTOM,
chatConversationId,
top,
bottom,
})
/**
@ -56,7 +56,7 @@ export const scrollBottomChatMessageConversation = (chatConversationId, top) =>
export const expandChatMessages = (chatConversationId, params = {}, done = noop) => (dispatch, getState) => {
if (!me || !chatConversationId) return
const chatConversation = getState().getIn(['chat_messages', chatConversationId], ImmutableMap())
const chatConversation = getState().getIn(['chat_conversations', chatConversationId], ImmutableMap())
const isLoadingMore = !!params.maxId
if (!!chatConversation && (chatConversation.get('isLoading') || chatConversation.get('isError'))) {

@ -45,6 +45,22 @@ export const CHAT_CONVERSATION_DELETE_REQUEST = 'CHAT_CONVERSATION_DELETE_REQUES
export const CHAT_CONVERSATION_DELETE_SUCCESS = 'CHAT_CONVERSATION_DELETE_SUCCESS'
export const CHAT_CONVERSATION_DELETE_FAIL = 'CHAT_CONVERSATION_DELETE_FAIL'
//
export const CHAT_CONVERSATION_MARK_READ_FETCH = 'CHAT_CONVERSATION_MARK_READ_FETCH'
export const CHAT_CONVERSATION_MARK_READ_SUCCESS = 'CHAT_CONVERSATION_MARK_READ_SUCCESS'
export const CHAT_CONVERSATION_MARK_READ_FAIL = 'CHAT_CONVERSATION_MARK_READ_FAIL'
export const CHAT_CONVERSATION_HIDE_FETCH = 'CHAT_CONVERSATION_HIDE_FETCH'
export const CHAT_CONVERSATION_HIDE_SUCCESS = 'CHAT_CONVERSATION_HIDE_SUCCESS'
export const CHAT_CONVERSATION_HIDE_FAIL = 'CHAT_CONVERSATION_HIDE_FAIL'
//
export const SET_CHAT_CONVERSATION_EXPIRATION_REQUEST = 'SET_CHAT_CONVERSATION_EXPIRATION_REQUEST'
export const SET_CHAT_CONVERSATION_EXPIRATION_SUCCESS = 'SET_CHAT_CONVERSATION_EXPIRATION_SUCCESS'
export const SET_CHAT_CONVERSATION_EXPIRATION_FAIL = 'SET_CHAT_CONVERSATION_EXPIRATION_FAIL'
/**
* @description Fetch paginated active chat conversations, import accounts and set chat converations
*/
@ -309,4 +325,93 @@ export const approveChatConversationRequestSuccess = (chatConversation) => ({
export const approveChatConversationRequestFail = () => ({
type: CHAT_CONVERSATION_REQUEST_APPROVE_FAIL,
})
})
/**
*
*/
export const hideChatConversation = (chatConversationId) => (dispatch, getState) => {
if (!me|| !chatConversationId) return
dispatch(hideChatConversationFetch(chatConversationId))
api(getState).post(`/api/v1/chat_conversation/${chatConversationId}/mark_chat_conversation_hidden`).then((response) => {
dispatch(approveChatConversationRequestSuccess(chatConversationId))
}).catch((error) => dispatch(approveChatConversationRequestFail(error)))
}
export const hideChatConversationFetch = (chatConversationId) => ({
type: CHAT_CONVERSATION_HIDE_SUCCESS,
chatConversationId,
})
export const hideChatConversationSuccess = (chatConversationId) => ({
type: CHAT_CONVERSATION_HIDE_SUCCESS,
chatConversationId,
})
export const hideChatConversationFail = () => ({
type: CHAT_CONVERSATION_HIDE_FAIL,
})
/**
*
*/
export const readChatConversation = (chatConversationId) => (dispatch, getState) => {
if (!me|| !chatConversationId) return
const chatConversation = getState().getIn(['chat_conversations', chatConversationId])
if (!chatConversation) return
if (chatConversation.get('unread_count') < 1) return
dispatch(readChatConversationFetch(chatConversation))
api(getState).post(`/api/v1/chat_conversation/${chatConversationId}/mark_chat_conversation_read`).then((response) => {
dispatch(readChatConversationSuccess(response.data))
}).catch((error) => dispatch(readChatConversationFail(error)))
}
export const readChatConversationFetch = (chatConversation) => ({
type: CHAT_CONVERSATION_MARK_READ_FETCH,
chatConversation,
})
export const readChatConversationSuccess = (chatConversation) => ({
type: CHAT_CONVERSATION_MARK_READ_SUCCESS,
chatConversation,
})
export const readChatConversationFail = () => ({
type: CHAT_CONVERSATION_MARK_READ_FAIL,
})
/**
*
*/
export const setChatConversationExpiration = (chatConversationId, expiration) => (dispatch, getState) => {
if (!me|| !chatConversationId || !expiration) return
dispatch(setChatConversationExpirationFetch(chatConversation))
api(getState).post(`/api/v1/chat_conversation/${chatConversationId}/set_expiration_policy`, {
expiration,
}).then((response) => {
dispatch(setChatConversationExpirationSuccess(response.data))
}).catch((error) => dispatch(setChatConversationExpirationFail(error)))
}
export const setChatConversationExpirationFetch = (chatConversation) => ({
type: SET_CHAT_CONVERSATION_EXPIRATION_REQUEST,
chatConversation,
})
export const setChatConversationExpirationSuccess = (chatConversation) => ({
type: SET_CHAT_CONVERSATION_EXPIRATION_REQUEST,
chatConversation,
})
export const setChatConversationExpirationFail = (error) => ({
type: SET_CHAT_CONVERSATION_EXPIRATION_REQUEST,
error,
})

@ -56,6 +56,22 @@ const sendChatMessageFail = (error) => ({
error,
})
/**
*
*/
export const manageIncomingChatMessage = (chatMessage) => (dispatch, getState) => {
if (!chatMessage) return
console.log("chatMessage:", chatMessage)
dispatch(sendChatMessageSuccess(chatMessage))
const isOnline = getState().getIn(['chat_conversation_messages', chatMessage.chat_conversation_id, 'online'])
console.log("isOnline: ", isOnline)
// : todo :
// Check if is online for conversation, if not increase total/convo unread count
}
/**
*
*/
@ -99,24 +115,25 @@ export const purgeChatMessages = (chatConversationId) => (dispatch, getState) =>
dispatch(deleteChatMessagesRequest(chatConversationId))
api(getState).delete(`/api/v1/chat_conversations/${chatConversationId}/messages/destroy_all`).then((response) => {
api(getState).delete(`/api/v1/chat_conversations/messages/${chatConversationId}/destroy_all`).then((response) => {
dispatch(deleteChatMessagesSuccess(response.data))
}).catch((error) => {
dispatch(deleteChatMessagesFail(error))
})
}
const deleteChatMessagesRequest = () => ({
const purgeChatMessagesRequest = (chatConversationId) => ({
type: CHAT_MESSAGES_PURGE_REQUEST,
chatConversationId,
})
const deleteChatMessagesSuccess = (chatConversationId) => ({
const purgeChatMessagesSuccess = (chatConversationId) => ({
type: CHAT_MESSAGES_PURGE_SUCCESS,
chatConversationId,
showToast: true,
})
const deleteChatMessagesFail = (error) => ({
const purgeChatMessagesFail = (error) => ({
type: CHAT_MESSAGES_PURGE_FAIL,
showToast: true,
error,

@ -20,12 +20,12 @@ import { defineMessages } from 'react-intl'
import { openModal, closeModal } from './modal'
import {
MODAL_COMPOSE,
STATUS_EXPIRATION_OPTION_5_MINUTES,
STATUS_EXPIRATION_OPTION_60_MINUTES,
STATUS_EXPIRATION_OPTION_6_HOURS,
STATUS_EXPIRATION_OPTION_24_HOURS,
STATUS_EXPIRATION_OPTION_3_DAYS,
STATUS_EXPIRATION_OPTION_7_DAYS,
EXPIRATION_OPTION_5_MINUTES,
EXPIRATION_OPTION_60_MINUTES,
EXPIRATION_OPTION_6_HOURS,
EXPIRATION_OPTION_24_HOURS,
EXPIRATION_OPTION_3_DAYS,
EXPIRATION_OPTION_7_DAYS,
} from '../constants'
import { me } from '../initial_state'
import { makeGetStatus } from '../selectors'
@ -347,17 +347,17 @@ export const submitCompose = (groupId, replyToId = null, router, isStandalone, a
let expires_at = getState().getIn(['compose', 'expires_at'], null)
if (expires_at) {
if (expires_at === STATUS_EXPIRATION_OPTION_5_MINUTES) {
if (expires_at === EXPIRATION_OPTION_5_MINUTES) {
expires_at = moment.utc().add('5', 'minute').toDate()
} else if (expires_at === STATUS_EXPIRATION_OPTION_60_MINUTES) {
} else if (expires_at === EXPIRATION_OPTION_60_MINUTES) {
expires_at = moment.utc().add('60', 'minute').toDate()
} else if (expires_at === STATUS_EXPIRATION_OPTION_6_HOURS) {
} else if (expires_at === EXPIRATION_OPTION_6_HOURS) {
expires_at = moment.utc().add('6', 'hour').toDate()
} else if (expires_at === STATUS_EXPIRATION_OPTION_24_HOURS) {
} else if (expires_at === EXPIRATION_OPTION_24_HOURS) {
expires_at = moment.utc().add('24', 'hour').toDate()
} else if (expires_at === STATUS_EXPIRATION_OPTION_3_DAYS) {
} else if (expires_at === EXPIRATION_OPTION_3_DAYS) {
expires_at = moment.utc().add('3', 'day').toDate()
} else if (expires_at === STATUS_EXPIRATION_OPTION_7_DAYS) {
} else if (expires_at === EXPIRATION_OPTION_7_DAYS) {
expires_at = moment.utc().add('7', 'day').toDate()
}
}

@ -6,7 +6,7 @@ import {
updateTimelineQueue,
} from './timelines'
import { updateNotificationsQueue } from './notifications'
import { sendChatMessageSuccess } from './chat_messages'
import { manageIncomingChatMessage } from './chat_messages'
import { fetchFilters } from './filters'
import { getLocale } from '../locales'
import { handleComposeSubmit } from './compose'
@ -84,7 +84,7 @@ export const connectChatMessagesStream = (accountId) => {
onReceive (data) {
if (!data['event'] || !data['payload']) return
if (data.event === 'notification') {
dispatch(sendChatMessageSuccess(JSON.parse(data.payload)))
dispatch(manageIncomingChatMessage(JSON.parse(data.payload)))
}
},
}

@ -189,6 +189,7 @@ class AutosuggestTextbox extends ImmutablePureComponent {
id,
isPro,
isEdit,
isModalOpen,
} = this.props
const { suggestionsHidden } = this.state

@ -1,29 +1,24 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
FormattedMessage,
defineMessages,
injectIntl,
} from 'react-intl'
import { openModal } from '../actions/modal'
import {
me,
repository,
source_url,
me,
} from '../initial_state'
import { CX, DEFAULT_REL } from '../constants'
import { DEFAULT_REL } from '../constants'
import Text from './text'
import Button from './button'
import DotTextSeperator from './dot_text_seperator'
class LinkFooter extends React.PureComponent {
render() {
const {
intl,
noPadding,
onOpenHotkeys,
} = this.props
const { intl } = this.props
const currentYear = new Date().getFullYear()
@ -32,12 +27,6 @@ class LinkFooter extends React.PureComponent {
href: 'https://help.gab.com',
text: intl.formatMessage(messages.help),
},
// : todo :
// {
// onClick: onOpenHotkeys,
// text: intl.formatMessage(messages.hotkeys),
// requiresUser: true,
// },
{
href: '/auth/edit',
text: intl.formatMessage(messages.security),
@ -52,16 +41,16 @@ class LinkFooter extends React.PureComponent {
text: intl.formatMessage(messages.investors),
},
{
to: '/about/tos',
text: intl.formatMessage(messages.terms),
to: '/about/sales',
text: intl.formatMessage(messages.salesTerms),
},
{
to: '/about/dmca',
text: intl.formatMessage(messages.dmca),
},
{
to: '/about/sales',
text: intl.formatMessage(messages.salesTerms),
to: '/about/tos',
text: intl.formatMessage(messages.terms),
},
{
to: '/about/privacy',
@ -75,36 +64,33 @@ class LinkFooter extends React.PureComponent {
},
]
const containerClasses = CX({
d: 1,
px10: !noPadding,
mb15: 1,
})
return (
<div className={containerClasses}>
<div className={[_s.d, _s.mb15].join(' ')}>
<nav aria-label='Footer' role='navigation' className={[_s.d, _s.flexWrap, _s.flexRow].join(' ')}>
{
linkFooterItems.map((linkFooterItem, i) => {
if (linkFooterItem.requiresUser && !me) return null
return (
<Button
isText
underlineOnHover
color='none'
backgroundColor='none'
key={`link-footer-item-${i}`}
to={linkFooterItem.to}
href={linkFooterItem.href}
data-method={linkFooterItem.logout ? 'delete' : null}
onClick={linkFooterItem.onClick || null}
className={[_s.mt5, _s.mb5, _s.pr15].join(' ')}
>
<Text size='small' color='tertiary'>
{linkFooterItem.text}
</Text>
</Button>
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter].join(' ')}>
<Button
isText
underlineOnHover
color='none'
backgroundColor='none'
key={`link-footer-item-${i}`}
to={linkFooterItem.to}
href={linkFooterItem.href}
data-method={linkFooterItem.logout ? 'delete' : null}
onClick={linkFooterItem.onClick || null}
className={[_s.mt5].join(' ')}
>
<Text size='small' color='tertiary'>
{linkFooterItem.text}
</Text>
</Button>
{ !linkFooterItem.logout && <Text size='small' color='secondary' className={[_s.pt2, _s.mr5, _s.ml5].join(' ')}>·</Text> }
</div>
)
})
}
@ -120,7 +106,7 @@ class LinkFooter extends React.PureComponent {
defaultMessage='Gab Social is open source software. You can contribute or report issues on our self-hosted GitLab at {gitlab}.'
values={{
gitlab: (
<a href={source_url} className={[_s.displayBlock, _s.inherit].join(' ')} rel={DEFAULT_REL} target='_blank'>
<a href={source_url} className={[_s.displayInlineBlock, _s.inherit].join(' ')} rel={DEFAULT_REL} target='_blank'>
{repository}
</a>
)
@ -136,8 +122,6 @@ class LinkFooter extends React.PureComponent {
const messages = defineMessages({
investors: { id: 'getting_started.investors', defaultMessage: 'Investors' },
help: { id: 'getting_started.help', defaultMessage: 'Help' },
invite: { id: 'getting_started.invite', defaultMessage: 'Invite people' },
hotkeys: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Hotkeys' },
security: { id: 'getting_started.security', defaultMessage: 'Security' },
about: { id: 'navigation_bar.info', defaultMessage: 'About' },
developers: { id: 'getting_started.developers', defaultMessage: 'Developers' },
@ -148,16 +132,8 @@ const messages = defineMessages({
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
})
const mapDispatchToProps = (dispatch) => ({
onOpenHotkeys() {
dispatch(openModal('HOTKEYS'))
},
})
LinkFooter.propTypes = {
intl: PropTypes.object.isRequired,
noPadding: PropTypes.bool,
onOpenHotkeys: PropTypes.func.isRequired,
}
export default injectIntl(connect(null, mapDispatchToProps)(LinkFooter))
export default injectIntl(LinkFooter)

@ -79,7 +79,7 @@ class MediaItem extends ImmutablePureComponent {
posAbs: 1,
top0: 1,
h100PC: 1,
w100PC: 1,
// w100PC: 1,
py2: !isSmall,
px2: !isSmall,
})
@ -87,7 +87,7 @@ class MediaItem extends ImmutablePureComponent {
const linkClasses = CX({
d: 1,
w100PC: 1,
h100PC: 1,
// h100PC: 1,
overflowHidden: 1,
border1PX: 1,
borderColorPrimary: 1,
@ -96,7 +96,7 @@ class MediaItem extends ImmutablePureComponent {
const statusUrl = `/${account.getIn(['acct'])}/posts/${status.get('id')}`;
return (
<div className={[_s.d, _s.w25PC, _s.pt25PC].join(' ')}>
<div className={[_s.d, _s.pt25PC].join(' ')}>
<div className={containerClasses}>
<NavLink
to={statusUrl}
@ -117,6 +117,7 @@ class MediaItem extends ImmutablePureComponent {
visible &&
<Image
height='100%'
width=''
src={attachment.get('preview_url')}
alt={attachment.get('description')}
title={attachment.get('description')}

@ -49,9 +49,9 @@ class ComposeModal extends ImmutablePureComponent {
const title = isEditing ? messages.edit : isComment ? messages.comment : messages.title
return (
<div style={{width: '512px'}} className={[_s.d, _s.modal].join(' ')}>
<div style={{width: '580px'}} className={[_s.d, _s.modal].join(' ')}>
<Block>
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.h53PX, _s.pl10, _s.pr15].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.h53PX, _s.pl5, _s.pr10].join(' ')}>
<div className={[_s.d, _s.w115PX, _s.aiStart, _s.jcCenter, _s.mrAuto].join(' ')}>
<Button
backgroundColor='none'
@ -69,8 +69,8 @@ class ComposeModal extends ImmutablePureComponent {
<ComposeFormSubmitButton type='header' />
</div>
</div>
<div className={[_s.d].join(' ')}>
<TimelineComposeBlock isModal />
<div className={[_s.d, _s.pt5].join(' ')}>
<TimelineComposeBlock isModal formLocation='modal' />
</div>
</Block>
</div>

@ -1,6 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { MODAL_DECK_COLUMN_ADD_OPTIONS } from '../../constants'
import { setDeckColumnAtIndex } from '../../actions/deck'
import { openModal } from '../../actions/modal'
import ModalLayout from './modal_layout'
@ -10,27 +11,19 @@ import Text from '../text'
class DeckColumnAddModal extends React.PureComponent {
onAdd = (column) => {
console.log("onAdd column: ", column)
switch (column) {
case 'user':
//
break
case 'list':
//
break
case 'group':
//
break
case 'hashtag':
//
break
default:
this.props.dispatch(setDeckColumnAtIndex(column))
this.props.onClose()
break
const moreOptions = ['user', 'list', 'group', 'hashtag']
if (moreOptions.indexOf(column) > -1) {
this.openOptionsModal(column)
} else {
this.props.dispatch(setDeckColumnAtIndex(column))
this.props.onClose()
}
}
openOptionsModal = (column) => {
this.props.dispatch(openModal(MODAL_DECK_COLUMN_ADD_OPTIONS, { column }))
}
render() {
const {
intl,

@ -0,0 +1,75 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { openModal } from '../../actions/modal'
import { MODAL_DECK_COLUMN_ADD } from '../../constants'
import Heading from '../heading'
import Button from '../button'
import Block from '../block'
class DeckColumnAddOptionsModal extends React.PureComponent {
state = {
selectedItem: null,
}
onClickClose = () => {
this.props.onClose()
this.props.dispatch(openModal(MODAL_DECK_COLUMN_ADD))
}
handleAdd = () => {
//
}
render() {
const { column } = this.props
const { selectedItem } = this.state
// user, hashtag, list, groups
if (!column) return <div />
const title = `Select a ${column}`
return (
<div style={{width: '520px'}} className={[_s.d, _s.modal].join(' ')}>
<Block>
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.jcCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.h53PX, _s.pl10, _s.pr15].join(' ')}>
<div className={[_s.d, _s.w115PX, _s.aiStart, _s.jcCenter, _s.mrAuto].join(' ')}>
<Button
backgroundColor='none'
title='Back'
onClick={this.onClickClose}
color='secondary'
icon='back'
iconSize='16px'
/>
</div>
<Heading size='h2'>
{title}
</Heading>
<div className={[_s.d, _s.w115PX, _s.aiEnd, _s.jcCenter, _s.mlAuto].join(' ')}>
<Button
isDisabled={!selectedItem}
onClick={this.handleAdd}
>
Add
</Button>
</div>
</div>
<div className={[_s.d].join(' ')}>
test
</div>
</Block>
</div>
)
}
}
DeckColumnAddOptionsModal.propTypes = {
onClose: PropTypes.func.isRequired,
column: PropTypes.string.isRequired,
}
export default connect()(DeckColumnAddOptionsModal)

@ -16,6 +16,7 @@ import {
MODAL_COMPOSE,
MODAL_CONFIRM,
MODAL_DECK_COLUMN_ADD,
MODAL_DECK_COLUMN_ADD_OPTIONS,
MODAL_DISPLAY_OPTIONS,
MODAL_EDIT_PROFILE,
MODAL_EDIT_SHORTCUTS,
@ -51,6 +52,7 @@ import {
ComposeModal,
ConfirmationModal,
DeckColumnAddModal,
DeckColumnAddOptionsModal,
DisplayOptionsModal,
EditProfileModal,
EditShortcutsModal,
@ -89,6 +91,7 @@ const MODAL_COMPONENTS = {
[MODAL_COMPOSE]: ComposeModal,
[MODAL_CONFIRM]: ConfirmationModal,
[MODAL_DECK_COLUMN_ADD]: DeckColumnAddModal,
[MODAL_DECK_COLUMN_ADD_OPTIONS]: DeckColumnAddOptionsModal,
[MODAL_DISPLAY_OPTIONS]: DisplayOptionsModal,
[MODAL_EDIT_SHORTCUTS]: EditShortcutsModal,
[MODAL_EDIT_PROFILE]: EditProfileModal,

@ -49,7 +49,7 @@ class ChatNavigationBar extends React.PureComponent {
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.jcCenter, _s.aiCenter, _s.mrAuto].join(' ')}>
<AvatarGroup accounts={otherAccounts} size={35} noHover />
<Heading size='h1'>
<div className={[_s.dangerousContent, _s.pl10, _s.fs19PX].join(' ')} dangerouslySetInnerHTML={{ __html: nameHTML }} />
<div className={[_s.dangerousContent, _s.colorNavigation, _s.pl10, _s.fs19PX].join(' ')} dangerouslySetInnerHTML={{ __html: nameHTML }} />
</Heading>
</div>

@ -8,7 +8,7 @@ import Heading from '../heading'
import Button from '../button'
import BackButton from '../back_button'
import Text from '../text'
import CharacterCounter from '../character_counter'
import ComposeFormSubmitButton from '../../features/compose/components/compose_form_submit_button'
class ComposeNavigationBar extends React.PureComponent {
@ -26,13 +26,7 @@ class ComposeNavigationBar extends React.PureComponent {
} = this.props
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
const buttonOptions = {
backgroundColor: disabledButton ? 'tertiary' : 'brand',
color: disabledButton ? 'tertiary' : 'white',
isDisabled: disabledButton,
onClick: this.handleOnPost,
}
return (
<div className={[_s.d, _s.z4, _s.h53PX, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.h53PX, _s.bgNavigation, _s.aiCenter, _s.z3, _s.top0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
@ -48,18 +42,14 @@ class ComposeNavigationBar extends React.PureComponent {
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.jcCenter, _s.aiCenter, _s.mrAuto].join(' ')}>
<Heading size='h1'>
Compose
<span className={[_s.dangerousContent, _s.fs24PX, _s.colorNavigation].join(' ')}>
Compose
</span>
</Heading>
</div>
<div className={[_s.d, _s.h53PX, _s.flexRow, _s.mlAuto, _s.aiCenter, _s.jcCenter, _s.mr15].join(' ')}>
<CharacterCounter max={MAX_POST_CHARACTER_COUNT} text={text} />
<Button {...buttonOptions}>
<Text color='inherit' weight='bold' size='medium' className={_s.px5}>
POST
</Text>
</Button>
<ComposeFormSubmitButton type='navigation' />
</div>
</div>

@ -61,7 +61,7 @@ class MediaGalleryPanel extends ImmutablePureComponent {
noPadding
title={intl.formatMessage(messages.title)}
headerButtonTitle={!!account ? intl.formatMessage(messages.show_all) : undefined}
headerButtonTo={!!account ? `/${account.get('acct')}/media` : undefined}
headerButtonTo={!!account ? `/${account.get('acct')}/photos` : undefined}
>
<div className={[_s.d, _s.flexRow, _s.flexWrap, _s.px10, _s.py10].join(' ')}>
{

@ -0,0 +1,135 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import { closePopover } from '../../actions/popover'
import { changeExpiresAt } from '../../actions/compose'
import {
EXPIRATION_OPTION_5_MINUTES,
EXPIRATION_OPTION_60_MINUTES,
EXPIRATION_OPTION_6_HOURS,
EXPIRATION_OPTION_24_HOURS,
EXPIRATION_OPTION_3_DAYS,
EXPIRATION_OPTION_7_DAYS,
} from '../../constants'
import PopoverLayout from './popover_layout'
import List from '../list'
class ChatConversationExpirationOptionsPopover extends React.PureComponent {
handleOnSetExpiration = (expiresAt) => {
this.props.onChangeExpiresAt(expiresAt)
this.handleOnClosePopover()
}
handleOnClosePopover = () => {
this.props.onClosePopover()
}
render() {
const {
expiresAtValue,
intl,
isXS,
} = this.props
const listItems = [
{
hideArrow: true,
title: 'None',
onClick: () => this.handleOnSetStatusExpiration(null),
isActive: !expiresAtValue,
},
{
hideArrow: true,
title: intl.formatMessage(messages.minutes, { number: 5 }),
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_5_MINUTES),
isActive: expiresAtValue === EXPIRATION_OPTION_5_MINUTES,
},
{
hideArrow: true,
title: intl.formatMessage(messages.minutes, { number: 60 }),
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_60_MINUTES),
isActive: expiresAtValue === EXPIRATION_OPTION_60_MINUTES,
},
{
hideArrow: true,
title: '6 hours',
title: intl.formatMessage(messages.hours, { number: 6 }),
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_6_HOURS),
isActive: expiresAtValue === EXPIRATION_OPTION_6_HOURS,
},
{
hideArrow: true,
title: intl.formatMessage(messages.hours, { number: 24 }),
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_24_HOURS),
isActive: expiresAtValue === EXPIRATION_OPTION_24_HOURS,
},
{
hideArrow: true,
title: '3 days',
title: intl.formatMessage(messages.days, { number: 3 }),
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_3_DAYS),
isActive: expiresAtValue === EXPIRATION_OPTION_3_DAYS,
},
{
hideArrow: true,
title: intl.formatMessage(messages.days, { number: 7 }),
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_7_DAYS),
isActive: expiresAtValue === EXPIRATION_OPTION_7_DAYS,
},
]
if (expiresAtValue) {
listItems.unshift({
hideArrow: true,
title: 'Remove expiration',
onClick: () => this.handleOnSetStatusExpiration(null),
},)
}
return (
<PopoverLayout
width={210}
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>This chats delete after:</Text>
<List
scrollKey='chat_conversation_expiration'
items={listItems}
size={isXS ? 'large' : 'small'}
/>
</PopoverLayout>
)
}
}
const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
})
const mapStateToProps = (state) => ({
expiresAtValue: state.getIn(['compose', 'expires_at']),
})
const mapDispatchToProps = (dispatch) => ({
onChangeExpiresAt(expiresAt) {
dispatch(changeExpiresAt(expiresAt))
},
onClosePopover() {
dispatch(closePopover())
},
})
ChatConversationExpirationOptionsPopover.defaultProps = {
expiresAtValue: PropTypes.string.isRequired,
intl: PropTypes.object.isRequired,
isXS: PropTypes.bool,
onChangeExpiresAt: PropTypes.func.isRequired,
}
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ChatConversationExpirationOptionsPopover))

@ -5,14 +5,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'
import { connect } from 'react-redux'
import { closePopover } from '../../actions/popover'
import { openModal } from '../../actions/modal'
import {
isChatMessengerBlocked,
isChatMessengerMuted,
blockChatMessenger,
unblockChatMessenger,
muteChatMessenger,
unmuteChatMessenger,
} from '../../actions/chat_conversation_accounts'
import { hideChatConversation } from '../../actions/chat_conversations'
import { purgeChatMessages } from '../../actions/chat_messages'
import { MODAL_PRO_UPGRADE } from '../../constants'
import { me } from '../../initial_state'
import { makeGetChatConversation } from '../../selectors'
@ -27,21 +21,6 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
this.handleOnClosePopover()
}
handleOnBlock = () => {
this.props.onBlock()
this.handleOnClosePopover()
}
handleOnUnblock = () => {
this.props.onUnblock()
this.handleOnClosePopover()
}
handleOnMute = () => {
this.props.onMute()
this.handleOnClosePopover()
}
handleOnUnmute = () => {
this.props.onUnute()
this.handleOnClosePopover()
@ -51,7 +30,7 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
if (!this.props.isPro) {
this.props.openProUpgradeModal()
} else {
this.props.onPurge()
this.props.onPurge(this.props.chatConversationId)
}
this.handleOnClosePopover()
@ -68,18 +47,6 @@ class ChatConversationOptionsPopover extends ImmutablePureComponent {
} = this.props
const items = [
{
hideArrow: true,
title: 'Block Messenger',
subtitle: 'The messenger will not be able to message you.',
onClick: () => this.handleOnBlock(),
},
{
hideArrow: true,
title: 'Mute Messenger',
subtitle: 'You will not be notified of new messsages',
onClick: () => this.handleOnMute(),
},
{
hideArrow: true,
title: 'Hide Conversation',
@ -123,6 +90,12 @@ const mapDispatchToProps = (dispatch) => ({
onSetCommentSortingSetting(type) {
dispatch(closePopover())
},
onPurge(chatConversationId) {
dispatch(purgeChatMessages(chatConversationId))
},
onHide(chatConversationId) {
dispatch(hideChatConversation(chatConversationId))
},
onClosePopover: () => dispatch(closePopover()),
})

@ -1,58 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { closePopover } from '../../actions/popover'
import { deleteChatMessage } from '../../actions/chat_messages'
import PopoverLayout from './popover_layout'
import Button from '../button'
import Text from '../text'
class ChatMessageDeletePopover extends React.PureComponent {
handleOnClick = () => {
this.props.onDeleteChatMessage(this.props.chatMessageId)
}
handleOnClosePopover = () => {
this.props.onClosePopover()
}
render() {
const { isXS } = this.props
return (
<PopoverLayout
width={96}
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<Button
onClick={this.handleOnClick}
color='primary'
backgroundColor='tertiary'
className={[_s.radiusSmall].join(' ')}
>
<Text align='center' color='inherit'>Remove</Text>
</Button>
</PopoverLayout>
)
}
}
const mapDispatchToProps = (dispatch) => ({
onDeleteChatMessage(chatMessageId) {
dispatch(deleteChatMessage(chatMessageId))
dispatch(closePopover())
},
onClosePopover() {
dispatch(closePopover())
},
})
ChatMessageDeletePopover.propTypes = {
isXS: PropTypes.bool,
chatMessageId: PropTypes.string.isRequired,
onDeleteChatMessage: PropTypes.func.isRequired,
}
export default connect(null, mapDispatchToProps)(ChatMessageDeletePopover)

@ -0,0 +1,139 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { closePopover } from '../../actions/popover'
import { deleteChatMessage } from '../../actions/chat_messages'
import {
isChatMessengerBlocked,
isChatMessengerMuted,
blockChatMessenger,
unblockChatMessenger,
muteChatMessenger,
unmuteChatMessenger,
reportChatMessage,
} from '../../actions/chat_conversation_accounts'
import { makeGetChatMessage } from '../../selectors'
import { me } from '../../initial_state'
import PopoverLayout from './popover_layout'
import Button from '../button'
import List from '../list'
import Text from '../text'
class ChatMessageOptionsPopover extends React.PureComponent {
handleOnDelete = () => {
this.props.onDeleteChatMessage(this.props.chatMessageId)
}
handleOnReport = () => {
this.props.onReportChatMessage(this.props.chatMessageId)
}
handleOnBlock = () => {
if (this.props.isBlocked) {
this.props.unblockChatMessenger(this.props.fromAccountId)
} else {
this.props.blockChatMessenger(this.props.fromAccountId)
}
}
handleOnMute = () => {
if (this.props.isMuted) {
this.props.unmuteChatMessenger(this.props.fromAccountId)
} else {
this.props.muteChatMessenger(this.props.fromAccountId)
}
}
handleOnClosePopover = () => {
this.props.onClosePopover()
}
render() {
const {
isXS,
isMine,
isMuted,
isBlocked,
} = this.props
const items = isMine ? [
{
hideArrow: true,
title: 'Delete Message',
onClick: () => this.handleOnDelete(),
}
] : [
{
hideArrow: true,
title: 'Report Messenger',
onClick: () => this.handleOnReport(),
},
{},
{
hideArrow: true,
title: isBlocked ? 'Unblock Messenger' : 'Block Messenger',
subtitle: isBlocked ? '' : 'The messenger will not be able to message you.',
onClick: () => this.handleOnBlock(),
},
{
hideArrow: true,
title: isMuted ? 'Unmute Messenger' : 'Mute Messenger',
subtitle: isMuted ? '' : 'You will not be notified of new messsages',
onClick: () => this.handleOnMute(),
},
]
return (
<PopoverLayout
width={isMine ? 160 : 200}
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<List items={items} />
</PopoverLayout>
)
}
}
const mapStateToProps = (state, { chatMessageId }) => ({
isMine: state.getIn(['chat_messages', chatMessageId, 'from_account_id']) === me,
fromAccountId: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
isBlocked: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
isMuted: state.getIn(['chat_messages', chatMessageId, 'from_account_id']),
})
const mapDispatchToProps = (dispatch) => ({
onDeleteChatMessage(chatMessageId) {
dispatch(deleteChatMessage(chatMessageId))
dispatch(closePopover())
},
onBlock(accountId) {
dispatch(blockChatMessenger(accountId))
},
onUnblock(accountId) {
dispatch(unblockChatMessenger(accountId))
},
onMute(accountId) {
dispatch(muteChatMessenger(accountId))
},
onUnmute(accountId) {
dispatch(unmuteChatMessenger(accountId))
},
onReportChatMessage(chatMessageId) {
dispatch(reportChatMessage(chatMessageId))
},
onClosePopover() {
dispatch(closePopover())
},
})
ChatMessageOptionsPopover.propTypes = {
isXS: PropTypes.bool,
chatMessageId: PropTypes.string.isRequired,
isBlocked: PropTypes.bool.isRequired,
isMuted: PropTypes.bool.isRequired,
onDeleteChatMessage: PropTypes.func.isRequired,
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageOptionsPopover)

@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { closePopover } from '../../actions/popover'
import PopoverLayout from './popover_layout'
import List from '../list'
import Text from '../text'
class ComposePostDesinationPopover extends React.PureComponent {
handleOnClosePopover = () => {
this.props.onClosePopover()
}
render() {
const {
isXS,
} = this.props
// TIMELINE
// GROUP - MY GROUPS
const items = [
{
hideArrow: true,
title: 'Timeline',
onClick: () => this.handleOnDelete(),
},
{
title: 'Group',
onClick: () => this.handleOnReport(),
},
]
return (
<PopoverLayout
width={180}
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>Post to:</Text>
<List items={items} />
</PopoverLayout>
)
}
}
const mapStateToProps = (state) => ({
//
})
const mapDispatchToProps = (dispatch) => ({
onClosePopover: () => dispatch(closePopover()),
})
ComposePostDesinationPopover.propTypes = {
isXS: PropTypes.bool,
onClosePopover: PropTypes.func.isRequired,
}
export default connect(mapStateToProps, mapDispatchToProps)(ComposePostDesinationPopover)

@ -1,8 +1,9 @@
import {
BREAKPOINT_EXTRA_SMALL,
POPOVER_CHAT_CONVERSATION_OPTIONS,
POPOVER_CHAT_MESSAGE_DELETE,
POPOVER_CHAT_MESSAGE_OPTIONS,
POPOVER_COMMENT_SORTING_OPTIONS,
POPOVER_COMPOSE_POST_DESTINATION,
POPOVER_DATE_PICKER,
POPOVER_EMOJI_PICKER,
POPOVER_GROUP_LIST_SORT_OPTIONS,
@ -23,8 +24,9 @@ import {
} from '../../constants'
import {
ChatConversationOptionsPopover,
ChatMessageDeletePopover,
ChatMessageOptionsPopover,
CommentSortingOptionsPopover,
ComposePostDesinationPopover,
DatePickerPopover,
EmojiPickerPopover,
GroupListSortOptionsPopover,
@ -59,8 +61,9 @@ const initialState = getWindowDimension()
const POPOVER_COMPONENTS = {
[POPOVER_CHAT_CONVERSATION_OPTIONS]: ChatConversationOptionsPopover,
[POPOVER_CHAT_MESSAGE_DELETE]: ChatMessageDeletePopover,
[POPOVER_CHAT_MESSAGE_OPTIONS]: ChatMessageOptionsPopover,
[POPOVER_COMMENT_SORTING_OPTIONS]: CommentSortingOptionsPopover,
[POPOVER_COMPOSE_POST_DESTINATION]: ComposePostDesinationPopover,
[POPOVER_DATE_PICKER]: DatePickerPopover,
[POPOVER_EMOJI_PICKER]: EmojiPickerPopover,
[POPOVER_GROUP_LIST_SORT_OPTIONS]: GroupListSortOptionsPopover,

@ -5,15 +5,16 @@ import { defineMessages, injectIntl } from 'react-intl'
import { closePopover } from '../../actions/popover'
import { changeExpiresAt } from '../../actions/compose'
import {
STATUS_EXPIRATION_OPTION_5_MINUTES,
STATUS_EXPIRATION_OPTION_60_MINUTES,
STATUS_EXPIRATION_OPTION_6_HOURS,
STATUS_EXPIRATION_OPTION_24_HOURS,
STATUS_EXPIRATION_OPTION_3_DAYS,
STATUS_EXPIRATION_OPTION_7_DAYS,
EXPIRATION_OPTION_5_MINUTES,
EXPIRATION_OPTION_60_MINUTES,
EXPIRATION_OPTION_6_HOURS,
EXPIRATION_OPTION_24_HOURS,
EXPIRATION_OPTION_3_DAYS,
EXPIRATION_OPTION_7_DAYS,
} from '../../constants'
import PopoverLayout from './popover_layout'
import List from '../list'
import Text from '../text'
class StatusExpirationOptionsPopover extends React.PureComponent {
@ -34,43 +35,49 @@ class StatusExpirationOptionsPopover extends React.PureComponent {
} = this.props
const listItems = [
{
hideArrow: true,
title: 'None',
onClick: () => this.handleOnSetStatusExpiration(null),
isActive: !expiresAtValue,
},
{
hideArrow: true,
title: intl.formatMessage(messages.minutes, { number: 5 }),
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_5_MINUTES),
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_5_MINUTES,
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_5_MINUTES),
isActive: expiresAtValue === EXPIRATION_OPTION_5_MINUTES,
},
{
hideArrow: true,
title: intl.formatMessage(messages.minutes, { number: 60 }),
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_60_MINUTES),
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_60_MINUTES,
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_60_MINUTES),
isActive: expiresAtValue === EXPIRATION_OPTION_60_MINUTES,
},
{
hideArrow: true,
title: '6 hours',
title: intl.formatMessage(messages.hours, { number: 6 }),
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_6_HOURS),
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_6_HOURS,
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_6_HOURS),
isActive: expiresAtValue === EXPIRATION_OPTION_6_HOURS,
},
{
hideArrow: true,
title: intl.formatMessage(messages.hours, { number: 24 }),
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_24_HOURS),
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_24_HOURS,
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_24_HOURS),
isActive: expiresAtValue === EXPIRATION_OPTION_24_HOURS,
},
{
hideArrow: true,
title: '3 days',
title: intl.formatMessage(messages.days, { number: 3 }),
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_3_DAYS),
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_3_DAYS,
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_3_DAYS),
isActive: expiresAtValue === EXPIRATION_OPTION_3_DAYS,
},
{
hideArrow: true,
title: intl.formatMessage(messages.days, { number: 7 }),
onClick: () => this.handleOnSetStatusExpiration(STATUS_EXPIRATION_OPTION_7_DAYS),
isActive: expiresAtValue === STATUS_EXPIRATION_OPTION_7_DAYS,
onClick: () => this.handleOnSetStatusExpiration(EXPIRATION_OPTION_7_DAYS),
isActive: expiresAtValue === EXPIRATION_OPTION_7_DAYS,
},
]
@ -88,8 +95,9 @@ class StatusExpirationOptionsPopover extends React.PureComponent {
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>This gab deletes after:</Text>
<List
scrollKey='group_list_sort_options'
scrollKey='status_expiration'
items={listItems}
size={isXS ? 'large' : 'small'}
/>

@ -5,6 +5,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import { openModal } from '../../actions/modal'
import { showToast } from '../../actions/toasts'
import { closePopover } from '../../actions/popover'
import PopoverLayout from './popover_layout'
import Button from '../button'
@ -31,6 +32,7 @@ class StatusSharePopover extends ImmutablePureComponent {
}
document.body.removeChild(textarea)
this.props.onShowCopyToast()
this.handleClosePopover()
}
@ -157,6 +159,9 @@ const messages = defineMessages({
const mapDispatchToProps = (dispatch) => ({
onClosePopover: () => dispatch(closePopover()),
onShowCopyToast() {
dispatch(showToast())
},
})
StatusSharePopover.propTypes = {

@ -49,6 +49,7 @@ class StatusVisibilityDropdown extends React.PureComponent {
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<Text className={[_s.d, _s.px15, _s.py10, _s.bgSecondary].join(' ')}>Status Visibility:</Text>
<div className={[_s.d].join(' ')}>
{
options.map((option, i) => {

@ -148,7 +148,7 @@ class StyleButton extends React.PureComponent {
px10: 1,
mr5: 1,
noSelect: 1,
bgSecondaryDark_onHover: 1,
bgSubtle_onHover: 1,
bgBrandLight: active,
bgTransparent: 1,
radiusSmall: 1,
@ -162,7 +162,7 @@ class StyleButton extends React.PureComponent {
onMouseDown={this.handleOnClick}
title={label}
>
<Icon id={icon} size='12px' className={_s[iconColor]} />
<Icon id={icon} size='16px' className={_s[iconColor]} />
</button>
)
}

@ -43,12 +43,16 @@ class DeckSidebar extends ImmutablePureComponent {
this.props.onOpenComposeModal()
}
scrollToItem = () => {
}
setAvatarNode = (c) => {
this.avatarNode = c
}
render() {
const { account, logoDisabled } = this.props
const { account, gabDeckOrder, logoDisabled } = this.props
const isPro = !!account ? account.get('is_pro') : false
@ -83,6 +87,22 @@ class DeckSidebar extends ImmutablePureComponent {
<Divider isSmall />
<div className={[_s.d, _s.aiCenter, _s.jcCenter].join(' ')}>
{
!!gabDeckOrder && gabDeckOrder.map((item, i) => (
<Button
isText
key={`gab-deck-sidebar-dot-${i}`}
onClick={this.scrollToItem}
backgroundColor='secondary'
className={[_s.mt5, _s.mb5, _s.px10, _s.py10, _s.circle].join(' ')}
icon='notifications'
iconClassName={_s.cPrimary}
/>
))
}
</div>
<Divider isSmall />
{ isPro && <NavigationBarButton title='&nbsp;' icon='add' onClick={this.handleOnOpenNewColumnModel} /> }
@ -119,6 +139,7 @@ const mapStateToProps = (state) => ({
account: makeGetAccount()(state, me),
theme: state.getIn(['settings', 'displayOptions', 'theme'], DEFAULT_THEME),
logoDisabled: state.getIn(['settings', 'displayOptions', 'logoDisabled'], false),
gabDeckOrder: state.getIn(['settings', 'gabDeckOrder']),
})
const mapDispatchToProps = (dispatch) => ({

@ -10,12 +10,14 @@ import ComposeFormContainer from '../features/compose/containers/compose_form_co
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
import Responsive from '../features/ui/util/responsive_component'
import Avatar from './avatar'
import Heading from './heading'
import Button from './button'
import Text from './text'
class TimelineComposeBlock extends ImmutablePureComponent {
render() {
const {
formLocation,
account,
size,
intl,
@ -27,7 +29,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
return (
<section className={_s.d}>
<div className={[_s.d, _s.flexRow].join(' ')}>
<ComposeFormContainer {...rest} isModal={isModal} />
<ComposeFormContainer {...rest} isModal={isModal} formLocation={formLocation} />
</div>
</section>
)
@ -39,17 +41,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
classNames={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden, _s.radiusSmall].join(' ')}
classNamesXS={[_s.d, _s.boxShadowBlock, _s.bgPrimary, _s.overflowHidden].join(' ')}
>
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
<div className={[_s.d, _s.bgSubtle, _s.borderTop1PX, _s.borderBottom1PX, _s.borderColorSecondary, _s.px15, _s.py2, _s.aiCenter, _s.flexRow].join(' ')}>
<div className={_s.mr10}>
<Avatar account={account} size={20} noHover />
</div>
<Heading size='h5'>
{intl.formatMessage(messages.createPost)}
</Heading>
</div>
</Responsive>
<ComposeFormContainer {...rest} />
<ComposeFormContainer {...rest} formLocation={formLocation} />
</ResponsiveClassesComponent>
</section>
)
@ -70,10 +62,12 @@ TimelineComposeBlock.propTypes = {
account: ImmutablePropTypes.map.isRequired,
size: PropTypes.number,
isModal: PropTypes.bool,
formLocation: PropTypes.string,
}
TimelineComposeBlock.defaultProps = {
size: 32,
formLocation: 'timeline',
}
export default injectIntl(connect(mapStateToProps)(TimelineComposeBlock))

@ -344,7 +344,7 @@ class Video extends ImmutablePureComponent {
this.video.play()
}
setTimeout(() => { // : hack :
this.video.requestPictureInPicture()
this.video.requestPictureInPicture()
}, 500)
} else {
document.exitPictureInPicture()

@ -10,7 +10,6 @@ export const BREAKPOINT_LARGE = 1280
export const BREAKPOINT_MEDIUM = 1160
export const BREAKPOINT_SMALL = 1080
export const BREAKPOINT_EXTRA_SMALL = 992
export const BREAKPOINT_EXTRA_EXTRA_SMALL = 767
export const MOUSE_IDLE_DELAY = 300
@ -26,8 +25,9 @@ export const URL_GAB_PRO = 'https://pro.gab.com'
export const PLACEHOLDER_MISSING_HEADER_SRC = '/original/missing.png'
export const POPOVER_CHAT_CONVERSATION_OPTIONS = 'CHAT_CONVERSATION_OPTIONS'
export const POPOVER_CHAT_MESSAGE_DELETE = 'CHAT_MESSAGE_DELETE'
export const POPOVER_CHAT_MESSAGE_OPTIONS = 'CHAT_MESSAGE_OPTIONS'
export const POPOVER_COMMENT_SORTING_OPTIONS = 'COMMENT_SORTING_OPTIONS'
export const POPOVER_COMPOSE_POST_DESTINATION = 'COMPOSE_POST_DESTINATION'
export const POPOVER_DATE_PICKER = 'DATE_PICKER'
export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER'
export const POPOVER_GROUP_LIST_SORT_OPTIONS = 'GROUP_LIST_SORT_OPTIONS'
@ -54,6 +54,7 @@ export const MODAL_COMMUNITY_TIMELINE_SETTINGS = 'COMMUNITY_TIMELINE_SETTINGS'
export const MODAL_COMPOSE = 'COMPOSE'
export const MODAL_CONFIRM = 'CONFIRM'
export const MODAL_DECK_COLUMN_ADD = 'DECK_COLUMN_ADD'
export const MODAL_DECK_COLUMN_ADD_OPTIONS = 'DECK_COLUMN_ADD_OPTIONS'
export const MODAL_DISPLAY_OPTIONS = 'DISPLAY_OPTIONS'
export const MODAL_EDIT_PROFILE = 'EDIT_PROFILE'
export const MODAL_EDIT_SHORTCUTS = 'EDIT_SHORTCUTS'
@ -130,12 +131,12 @@ export const GAB_COM_INTRODUCE_YOURSELF_GROUP_ID = '12'
export const MIN_ACCOUNT_CREATED_AT_ONBOARDING = 1594789200000 // 2020-07-15
export const STATUS_EXPIRATION_OPTION_5_MINUTES = '5-minutes'
export const STATUS_EXPIRATION_OPTION_60_MINUTES = '60-minutes'
export const STATUS_EXPIRATION_OPTION_6_HOURS = '6-hours'
export const STATUS_EXPIRATION_OPTION_24_HOURS = '24-hours'
export const STATUS_EXPIRATION_OPTION_3_DAYS = '3-days'
export const STATUS_EXPIRATION_OPTION_7_DAYS = '7-days'
export const EXPIRATION_OPTION_5_MINUTES = 'five_minutes'
export const EXPIRATION_OPTION_60_MINUTES = 'one_hour'
export const EXPIRATION_OPTION_6_HOURS = 'six_hours'
export const EXPIRATION_OPTION_24_HOURS = 'one_day'
export const EXPIRATION_OPTION_3_DAYS = 'three_days'
export const EXPIRATION_OPTION_7_DAYS = 'one_week'
export const GROUP_TIMELINE_SORTING_TYPE_HOT = 'hot'
export const GROUP_TIMELINE_SORTING_TYPE_NEWEST = 'newest'

@ -0,0 +1,64 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { fetchBookmarkCollections } from '../actions/bookmarks'
import ColumnIndicator from '../components/column_indicator'
import List from '../components/list'
class BookmarkCollections extends ImmutablePureComponent {
componentDidMount() {
this.props.onFetchBookmarkCollections()
}
render() {
const {
isLoading,
isError,
bookmarkCollections,
} = this.props
if (isError) {
return <ColumnIndicator type='error' message='Error fetching bookmark collections' />
}
const listItems = shortcuts.map((s) => ({
to: s.get('to'),
title: s.get('title'),
image: s.get('image'),
}))
return (
<List
scrollKey='bookmark-collections'
emptyMessage='You have no bookmark collections'
items={listItems}
showLoading={isLoading}
/>
)
}
}
const mapStateToProps = (state) => ({
isError: state.getIn(['bookmark_collections', 'isError']),
isLoading: state.getIn(['bookmark_collections', 'isLoading']),
shortcuts: state.getIn(['bookmark_collections', 'items']),
})
const mapDispatchToProps = (dispatch) => ({
onFetchBookmarkCollections() {
dispatch(fetchBookmarkCollections())
},
})
BookmarkCollections.propTypes = {
isLoading: PropTypes.bool.isRequired,
isError: PropTypes.bool.isRequired,
onFetchBookmarkCollections: PropTypes.func.isRequired,
bookmarkCollections: ImmutablePropTypes.list,
}
export default connect(mapStateToProps, mapDispatchToProps)(BookmarkCollections)

@ -1,75 +1,94 @@
import React from 'react'
import PropTypes from 'prop-types'
import { defineMessages, injectIntl } from 'react-intl'
import { connect } from 'react-redux'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { length } from 'stringz'
import { isMobile } from '../../../utils/is_mobile'
import { countableText } from '../../ui/util/counter'
import {
CX,
MAX_POST_CHARACTER_COUNT,
ALLOWED_AROUND_SHORT_CODE,
BREAKPOINT_EXTRA_SMALL,
BREAKPOINT_EXTRA_EXTRA_SMALL,
BREAKPOINT_MEDIUM,
MODAL_COMPOSE,
POPOVER_COMPOSE_POST_DESTINATION,
} from '../../../constants'
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
import Responsive from '../../ui/util/responsive_component'
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
import { openModal } from '../../../actions/modal'
import { openPopover } from '../../../actions/popover'
import Avatar from '../../../components/avatar'
import Button from '../../../components/button'
import EmojiPickerButton from './emoji_picker_button'
import PollButton from './poll_button'
import PollForm from './poll_form'
import SchedulePostButton from './schedule_post_button'
import SpoilerButton from './spoiler_button'
import ExpiresPostButton from './expires_post_button'
import RichTextEditorButton from './rich_text_editor_button'
import StatusContainer from '../../../containers/status_container'
import StatusVisibilityButton from './status_visibility_button'
import UploadButton from './media_upload_button'
import UploadForm from './upload_form'
import Input from '../../../components/input'
import Text from '../../../components/text'
import Icon from '../../../components/icon'
import ComposeExtraButtonList from './compose_extra_button_list'
import Text from '../../../components/text'
class ComposeDestinationHeader extends ImmutablePureComponent {
handleOnClick = () => {
this.props.onOpenPopover(this.desinationBtn)
}
handleOnExpand = () => {
this.props.onOpenModal()
}
setDestinationBtn = (c) => {
this.desinationBtn = c
}
render() {
const { account } = this.props
const { account, isModal } = this.props
const title = 'Post to timeline'
return (
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.bgPrimary, _s.borderBottom1PX, _s.borderTop1PX, _s.borderColorSecondary, _s.mb5, _s.mt5, _s.px15, _s.w100PC, _s.h40PX].join(' ')}>
<Avatar account={account} size={28} />
<div className={[_s.ml15].join(' ')}>
<Button
isNarrow
radiusSmall
backgroundColor='tertiary'
color='primary'
onClick={this.handleOnClick}
>
<Text color='inherit' size='small' className={_s.jcCenter}>
{title}
<Icon id='caret-down' size='8px' className={_s.ml5} />
</Text>
</Button>
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.bgPrimary, _s.w100PC, _s.h40PX, _s.pr15].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.pl15, _s.flexGrow1, _s.mrAuto, _s.h40PX].join(' ')}>
<Avatar account={account} size={28} />
<div className={[_s.ml15].join(' ')}>
<Button
isNarrow
isOutline
radiusSmall
buttonRef={this.setDestinationBtn}
backgroundColor='secondary'
color='primary'
onClick={this.handleOnClick}
className={[_s.border1PX, _s.borderColorPrimary].join(' ')}
>
<Text color='inherit' size='small' className={_s.jcCenter}>
{title}
<Icon id='caret-down' size='8px' className={_s.ml5} />
</Text>
</Button>
</div>
</div>
{
!isModal &&
<Button
isText
isNarrow
backgroundColor='none'
color='tertiary'
icon='fullscreen'
onClick={this.handleOnExpand}
/>
}
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
onOpenModal() {
dispatch(openModal(MODAL_COMPOSE))
},
onOpenPopover(targetRef) {
dispatch(openPopover(POPOVER_COMPOSE_POST_DESTINATION, {
targetRef,
position: 'bottom',
}))
},
})
ComposeDestinationHeader.propTypes = {
account: ImmutablePropTypes.map,
isModal: PropTypes.bool,
onOpenModal: PropTypes.func.isRequired,
onOpenPopover: PropTypes.func.isRequired,
}
export default ComposeDestinationHeader
export default connect(null, mapDispatchToProps)(ComposeDestinationHeader)

@ -22,14 +22,14 @@ class ComposeExtraButton extends React.PureComponent {
const containerClasses = CX({
d: 1,
mr5: 1,
jcCenter: 1,
h40PX: 1,
mr5: 1,
})
const btnClasses = CX({
d: 1,
circle: 1,
circle: small,
noUnderline: 1,
font: 1,
cursorPointer: 1,
@ -37,21 +37,25 @@ class ComposeExtraButton extends React.PureComponent {
outlineNone: 1,
bgTransparent: 1,
flexRow: 1,
aiCenter: 1,
// jcCenter: !small,
bgSubtle_onHover: !active,
bgBrandLight: active,
py10: 1,
px10: 1,
px10: small,
radiusSmall: !small,
})
const iconClasses = CX(iconClassName, {
const iconClasses = CX(active ? null : iconClassName, {
cSecondary: !active,
cWhite: active,
mr10: 1,
mr10: !small,
py2: small,
ml10: small,
ml10: !small,
px2: small,
})
const iconSize = !small ? '18px' : '16px'
const iconSize = '16px'
const textColor = !active ? 'primary' : 'white'
return (
@ -65,13 +69,13 @@ class ComposeExtraButton extends React.PureComponent {
backgroundColor='none'
iconClassName={iconClasses}
icon={icon}
iconSize={iconSize}
iconSize='16px'
buttonRef={!children ? buttonRef : undefined}
>
{ children }
{
!small &&
<Text color={textColor} weight='medium' className={[_s.pr5].join(' ')}>
<Text color={textColor} weight='medium' className={[_s.pr10].join(' ')}>
{title}
</Text>
}

@ -6,6 +6,7 @@ import {
} from '../../../constants'
import Responsive from '../../ui/util/responsive_component'
import ResponsiveClassesComponent from '../../ui/util/responsive_classes_component'
import Text from '../../../components/text'
import EmojiPickerButton from './emoji_picker_button'
import PollButton from './poll_button'
import SchedulePostButton from './schedule_post_button'
@ -22,6 +23,7 @@ class ComposeExtraButtonList extends React.PureComponent {
state = {
height: initialState.height,
width: initialState.width,
}
componentDidMount() {
@ -31,9 +33,9 @@ class ComposeExtraButtonList extends React.PureComponent {
}
handleResize = () => {
const { height } = getWindowDimension()
const { height, width } = getWindowDimension()
this.setState({ height })
this.setState({ height, width })
}
componentWillUnmount() {
@ -48,26 +50,33 @@ class ComposeExtraButtonList extends React.PureComponent {
edit,
hidePro,
isModal,
isStandalone,
formLocation,
} = this.props
const { height } = this.state
const { height, width } = this.state
const small = (height <= 660 || isModal) && !isStandalone
const isXS = width <= BREAKPOINT_EXTRA_SMALL
const isStandalone = formLocation === 'standalone'
const isTimeline = formLocation === 'timeline'
const small = (!isModal && isXS && !isStandalone) || isTimeline
console.log("small, formLocation:", small, formLocation)
const containerClasses = CX({
d: 1,
w100PC: 1,
bgPrimary: 1,
px15: 1,
py10: 1,
px5: 1,
py5: 1,
mb10: 1,
mtAuto: 1,
boxShadowBlockY: 1,
topLeftRadiusSmall: 1,
radiusSmall: 1,
borderTop1PX: 1,
borderBottom1PX: 1,
boxShadowBlock: 1,
borderColorSecondary: 1,
topRightRadiusSmall: 1,
flexRow: small,
overflowXScroll: small,
noScrollbar: small,
flexWrap: 1,
flexRow: (small || !isTimeline || isXS) && !isStandalone,
jcSpaceAround: isXS,
})
return (
@ -79,8 +88,8 @@ class ComposeExtraButtonList extends React.PureComponent {
<SpoilerButton small={small} />
{ !hidePro && !edit && <SchedulePostButton small={small} /> }
{ !hidePro && !edit && <ExpiresPostButton small={small} /> }
{ !hidePro && <RichTextEditorButton small={small} /> }
</div>
{ !hidePro && !isXS && <RichTextEditorButton small={small} /> }
</div>
)
}
}
@ -90,7 +99,7 @@ ComposeExtraButtonList.propTypes = {
edit: PropTypes.bool,
isMatch: PropTypes.bool,
isModal: PropTypes.bool,
isStandalone: PropTypes.bool,
formLocation: PropTypes.string,
}
export default ComposeExtraButtonList

@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import { NavLink } from 'react-router-dom'
import { defineMessages, injectIntl } from 'react-intl'
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
@ -11,7 +12,6 @@ import {
MAX_POST_CHARACTER_COUNT,
ALLOWED_AROUND_SHORT_CODE,
BREAKPOINT_EXTRA_SMALL,
BREAKPOINT_EXTRA_EXTRA_SMALL,
BREAKPOINT_MEDIUM,
} from '../../../constants'
import AutosuggestTextbox from '../../../components/autosuggest_textbox'
@ -62,80 +62,70 @@ class ComposeForm extends ImmutablePureComponent {
}
handleComposeFocus = () => {
this.setState({
composeFocused: true,
});
this.setState({ composeFocused: true })
}
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
this.handleSubmit()
}
}
handleClick = (e) => {
const { isStandalone, isModalOpen, shouldCondense } = this.props
const { isModalOpen, shouldCondense } = this.props
if (!this.form) return false
if (e.target) {
if (e.target.classList.contains('react-datepicker__time-list-item')) return false
}
if (!this.form.contains(e.target)) {
this.handleClickOutside()
} else {
// : todo :
// if mobile go to /compose else openModal
if (!isStandalone && !isModalOpen && !shouldCondense) {
this.props.openComposeModal()
return false
}
}
}
handleClickOutside = () => {
const { shouldCondense, scheduledAt, text, isModalOpen } = this.props;
const condensed = shouldCondense && !text;
const { shouldCondense, scheduledAt, text, isModalOpen } = this.props
const condensed = shouldCondense && !text
if (condensed && scheduledAt && !isModalOpen) { //Reset scheduled date if condensing
this.props.setScheduledAt(null);
this.props.setScheduledAt(null)
}
this.setState({
composeFocused: false,
});
this.setState({ composeFocused: false })
}
handleSubmit = () => {
// if (this.props.text !== this.autosuggestTextarea.textbox.value) {
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
// Update the state to match the current text
// this.props.onChange(this.autosuggestTextarea.textbox.value);
// this.props.onChange(this.autosuggestTextarea.textbox.value)
// }
// Submit disabled:
const { isSubmitting, isChangingUpload, isUploading, anyMedia, groupId, autoJoinGroup } = this.props
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('');
const fulltext = [this.props.spoilerText, countableText(this.props.text)].join('')
if (isSubmitting || isUploading || isChangingUpload || length(fulltext) > MAX_POST_CHARACTER_COUNT || (fulltext.length !== 0 && fulltext.trim().length === 0 && !anyMedia)) {
return;
return
}
this.props.onSubmit(groupId, this.props.replyToId, this.context.router, autoJoinGroup)
}
onSuggestionsClearRequested = () => {
this.props.onClearSuggestions();
this.props.onClearSuggestions()
}
onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token);
this.props.onFetchSuggestions(token)
}
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
this.props.onSuggestionSelected(tokenStart, token, value, ['text'])
}
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text'])
}
handleChangeSpoilerText = (value) => {
@ -143,11 +133,11 @@ class ComposeForm extends ImmutablePureComponent {
}
componentDidMount() {
document.addEventListener('click', this.handleClick, false);
document.addEventListener('click', this.handleClick, false)
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClick, false);
document.removeEventListener('click', this.handleClick, false)
}
componentDidUpdate(prevProps) {
@ -156,24 +146,24 @@ class ComposeForm extends ImmutablePureComponent {
// This statement does several things:
// - If we're beginning a reply, and,
// - Replying to zero or one users, places the cursor at the end of the textbox.
// - Replying to more than one user, selects any usernames past the first;
// - Replying to more than one user, selects any usernames past the first
// this provides a convenient shortcut to drop everyone else from the conversation.
if (this.props.focusDate !== prevProps.focusDate) {
let selectionEnd, selectionStart;
let selectionEnd, selectionStart
if (this.props.preselectDate !== prevProps.preselectDate) {
selectionEnd = this.props.text.length;
selectionStart = this.props.text.search(/\s/) + 1;
selectionEnd = this.props.text.length
selectionStart = this.props.text.search(/\s/) + 1
} else if (typeof this.props.caretPosition === 'number') {
selectionStart = this.props.caretPosition;
selectionEnd = this.props.caretPosition;
selectionStart = this.props.caretPosition
selectionEnd = this.props.caretPosition
} else {
selectionEnd = this.props.text.length;
selectionStart = selectionEnd;
selectionEnd = this.props.text.length
selectionStart = selectionEnd
}
// this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd);
// this.autosuggestTextarea.textbox.focus();
// this.autosuggestTextarea.textbox.setSelectionRange(selectionStart, selectionEnd)
// this.autosuggestTextarea.textbox.focus()
}
}
@ -190,7 +180,6 @@ class ComposeForm extends ImmutablePureComponent {
intl,
account,
onPaste,
showSearch,
anyMedia,
shouldCondense,
autoFocus,
@ -208,218 +197,151 @@ class ComposeForm extends ImmutablePureComponent {
isSubmitting,
isPro,
hidePro,
isStandalone,
dontOpenModal,
formLocation,
} = this.props
const disabled = isSubmitting
const text = [this.props.spoilerText, countableText(this.props.text)].join('');
const text = [this.props.spoilerText, countableText(this.props.text)].join('')
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth)
const shouldAutoFocus = autoFocus && !isMobile(window.innerWidth)
const parentContainerClasses = CX({
const containerClasses = CX({
d: 1,
w100PC: 1,
flexRow: !shouldCondense,
pb10: !shouldCondense,
pb10: 1,
calcMaxH410PX: 1,
minH200PX: isModalOpen && isMatch,
overflowYScroll: 1,
boxShadowBlock: 1,
borderTop1PX: 1,
borderColorSecondary: 1,
})
const childContainerClasses = CX({
d: 1,
flexWrap: 1,
overflowHidden: 1,
flex1: 1,
minH28PX: 1,
py2: shouldCondense,
aiEnd: shouldCondense,
flexRow: shouldCondense,
radiusSmall: shouldCondense,
bgSubtle: shouldCondense,
px5: shouldCondense,
})
const actionsContainerClasses = CX({
d: 1,
flexRow: 1,
aiCenter: !shouldCondense,
aiStart: shouldCondense,
mt10: !shouldCondense,
px10: !shouldCondense,
mlAuto: shouldCondense,
flexWrap: !shouldCondense,
})
const commentPublishBtnClasses = CX({
d: 1,
jcCenter: 1,
displayNone: length(this.props.text) === 0,
})
const textbox = (
<AutosuggestTextbox
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage((shouldCondense || !!reduxReplyToId) && isMatch ? messages.commentPlaceholder : messages.placeholder)}
disabled={disabled}
value={this.props.text}
valueMarkdown={this.props.markdown}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown}
onFocus={this.handleComposeFocus}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
small={shouldCondense}
isModalOpen={isModalOpen}
isPro={isPro}
isEdit={!!edit}
id='main-composer'
/>
)
if (shouldCondense) {
return (
<div className={[_s.d, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.w100PC, _s.aiCenter].join(' ')}>
<div className={[_s.d, _s.mr10].join(' ')}>
<Avatar account={account} size={28} noHover />
<Avatar account={account} size={30} noHover />
</div>
<div
className={[_s.d, _s.flexWrap, _s.overflowHidden, _s.flex1, _s.minH28PX, _s.py2, _s.aiEnd, _s.flexRow, _s.radiusSmall, _s.bgSubtle, _s.px5].join(' ')}
className={[_s.d, _s.flexWrap, _s.overflowHidden, _s.flex1, _s.minH28PX, _s.py5, _s.aiEnd, _s.flexRow, _s.radiusSmall, _s.bgSubtle, _s.px5].join(' ')}
ref={this.setForm}
onClick={this.handleClick}
>
<AutosuggestTextbox
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.commentPlaceholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown}
onFocus={this.handleComposeFocus}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
small={shouldCondense}
isPro={isPro}
isEdit={!!edit}
id='comment-composer'
/>
<div className={[_s.d, _s.flexRow, _s.aiStart, _s.mlAuto].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.mrAuto].join(' ')}>
<div className={commentPublishBtnClasses}>
<Button
isNarrow
onClick={this.handleSubmit}
isDisabled={disabledButton}
className={_s.px10}
>
{intl.formatMessage(scheduledAt ? messages.schedulePost : messages.post)}
</Button>
</div>
</div>
</div>
{ textbox }
{ isMatch && <ComposeFormSubmitButton type='comment' /> }
</div>
</div>
{
(isUploading || anyMedia) &&
(isUploading || anyMedia) && isMatch &&
<div className={[_s.d, _s.w100PC, _s.pl35, _s.mt5].join(' ')}>
<UploadForm replyToId={replyToId} isModalOpen={isModalOpen} />
<UploadForm isModalOpen={isModalOpen} />
</div>
}
</div>
)
}
if (isStandalone || isModalOpen) {
return (
<div className={[_s.d, _s.w100PC, _s.flexGrow1, _s.bgTertiary].join(' ')}>
<div className={[_s.d, _s.pb10, _s.calcMaxH370PX, _s.overflowYScroll, _s.boxShadowBlock, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
<ComposeDestinationHeader account={account} />
<div
className={[_s.d, _s.bgPrimary, _s.boxShadowBlock, _s.w100PC, _s.minH200PX, _s.pb10, _s.borderBottom1PX, _s.borderTop1PX, _s.borderColorSecondary].join(' ')}
ref={this.setForm}
onClick={this.handleClick}
>
{
!!reduxReplyToId && isModalOpen && isMatch &&
<div className={[_s.d, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer contextType='compose' id={reduxReplyToId} isChild />
</div>
}
{
!!spoiler &&
<div className={[_s.d, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
<Input
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
onChange={this.handleChangeSpoilerText}
disabled={!this.props.spoiler}
prependIcon='warning'
maxLength={256}
id='cw-spoiler-input'
/>
</div>
}
<AutosuggestTextbox
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage((shouldCondense || !!reduxReplyToId) && isMatch ? messages.commentPlaceholder : messages.placeholder)}
disabled={disabled}
value={this.props.text}
valueMarkdown={this.props.markdown}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown}
onFocus={this.handleComposeFocus}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
small={shouldCondense}
isPro={isPro}
isEdit={!!edit}
id='main-composer'
/>
{
(isUploading || anyMedia) &&
<div className={[_s.d, _s.px15, _s.mt5].join(' ')}>
<UploadForm replyToId={replyToId} isModalOpen={isModalOpen} />
</div>
}
{
!edit && hasPoll &&
<div className={[_s.d, _s.px15, _s.mt5].join(' ')}>
<PollForm replyToId={replyToId} />
</div>
}
{
!!quoteOfId && isModalOpen && isMatch &&
<div className={[_s.d, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer contextType='compose' id={quoteOfId} isChild />
</div>
}
</div>
</div>
{ !isModalOpen && <ComposeFormSubmitButton /> }
<ComposeExtraButtonList isStandalone={isStandalone} isMatch={isMatch} hidePro={hidePro} edit={edit} isModal={isModalOpen} />
</div>
)
}
return (
<div
className={[_s.d, _s.w100PC, _s.pb10, _s.flexWrap, _s.overflowHidden, _s.flex1].join(' ')}
ref={this.setForm}
onClick={this.handleClick}
>
<Text className={[_s.d, _s.px15, _s.pt15, _s.pb10].join(' ')} size='medium' color='tertiary'>
{intl.formatMessage((shouldCondense || !!reduxReplyToId) && isMatch ? messages.commentPlaceholder : messages.placeholder)}
</Text>
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.mt5, _s.px10, _s.flexWrap].join(' ')}>
<UploadButton />
<EmojiPickerButton isMatch={isMatch} />
<PollButton />
<MoreButton />
<ComposeFormSubmitButton />
<div className={[_s.d, _s.w100PC, _s.flexGrow1, _s.bgPrimary].join(' ')}>
<div className={[_s.d, _s.calcMaxH410PX, _s.overflowYScroll].join(' ')}>
<Responsive min={BREAKPOINT_EXTRA_SMALL}>
<ComposeDestinationHeader account={account} isModal={isModalOpen} />
</Responsive>
<div className={containerClasses} ref={this.setForm} onClick={this.handleClick}>
{
!!reduxReplyToId && isModalOpen && isMatch &&
<div className={[_s.d, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer contextType='compose' id={reduxReplyToId} isChild />
</div>
}
{
!!spoiler &&
<div className={[_s.d, _s.px15, _s.py10, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
<Input
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
onChange={this.handleChangeSpoilerText}
disabled={!this.props.spoiler}
prependIcon='warning'
maxLength={256}
id='cw-spoiler-input'
/>
</div>
}
{ textbox }
{
(isUploading || anyMedia) &&
<div className={[_s.d, _s.px15, _s.mt5].join(' ')}>
<div className={[_s.d, _s.borderTop1PX, _s.borderColorSecondary].join(' ')} />
<UploadForm />
</div>
}
{
!edit && hasPoll &&
<div className={[_s.d, _s.px15, _s.mt5].join(' ')}>
<PollForm />
</div>
}
{
!!quoteOfId && isModalOpen && isMatch &&
<div className={[_s.d, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer contextType='compose' id={quoteOfId} isChild />
</div>
}
</div>
</div>
<div className={[_s.d, _s.posAbs, _s.z2, _s.left0, _s.right0, _s.bottom0, _s.top0].join(' ')} />
<div className={[_s.d, _s.px10].join(' ')}>
<ComposeExtraButtonList formLocation={formLocation} isMatch={isMatch} hidePro={hidePro} edit={edit} isModal={isModalOpen} />
</div>
{
(!disabledButton || isModalOpen) && isMatch &&
<div className={[_s.d, _s.mb10, _s.px10].join(' ')}>
<ComposeFormSubmitButton type='block' />
</div>
}
<Responsive max={BREAKPOINT_EXTRA_SMALL}>
{
formLocation === 'timeline' &&
<NavLink to='/compose' className={[_s.d, _s.z4, _s.posAbs, _s.top0, _s.left0, _s.right0, _s.bottom0].join(' ')} />
}
</Responsive>
</div>
)
}
@ -450,9 +372,7 @@ ComposeForm.propTypes = {
onFetchSuggestions: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func.isRequired,
onChangeSpoilerText: PropTypes.func.isRequired,
openComposeModal: PropTypes.func.isRequired,
onPaste: PropTypes.func.isRequired,
showSearch: PropTypes.bool,
anyMedia: PropTypes.bool,
shouldCondense: PropTypes.bool,
autoFocus: PropTypes.bool,
@ -466,11 +386,7 @@ ComposeForm.propTypes = {
isPro: PropTypes.bool,
hidePro: PropTypes.bool,
autoJoinGroup: PropTypes.bool,
isStandalone: PropTypes.bool,
}
ComposeForm.defaultProps = {
showSearch: false,
formLocation: PropTypes.string,
}
export default injectIntl(ComposeForm)

@ -1,30 +1,80 @@
import React from 'react'
import PropTypes from 'prop-types'
import { CX } from '../../../constants'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import { length } from 'stringz'
import { countableText } from '../../ui/util/counter'
import { submitCompose } from '../../../actions/compose'
import {
CX,
MAX_POST_CHARACTER_COUNT,
} from '../../../constants'
import Button from '../../../components/button'
import Text from '../../../components/text'
class ComposeFormSubmitButton extends React.PureComponent {
handleSubmit = () => {
}
render() {
const {
intl,
title,
active,
small,
disabledButton,
type,
edit,
text,
isSubmitting,
isChangingUpload,
isUploading,
anyMedia,
quoteOfId,
scheduledAt,
hasPoll,
} = this.props
const disabledButton = isSubmitting || isUploading || isChangingUpload || length(text) > MAX_POST_CHARACTER_COUNT || (length(text.trim()) === 0 && !anyMedia)
if (type === 'comment') {
const commentPublishBtnClasses = CX({
d: 1,
jcCenter: 1,
displayNone: disabledButton,
})
return (
<div className={[_s.d, _s.flexRow, _s.aiStart, _s.mlAuto].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.mrAuto].join(' ')}>
<div className={commentPublishBtnClasses}>
<Button
isNarrow
radiusSmall
onClick={this.handleSubmit}
isDisabled={disabledButton}
className={_s.px15}
>
{intl.formatMessage(scheduledAt ? messages.schedulePost : messages.post)}
</Button>
</div>
</div>
</div>
)
}
const containerClasses = CX({
d: 1,
mr5: 1,
jcCenter: 1,
h40PX: 1,
})
const btnClasses = CX({
d: 1,
circle: 1,
radiusSmall: 1,
noUnderline: 1,
font: 1,
cursorPointer: 1,
@ -37,31 +87,33 @@ class ComposeFormSubmitButton extends React.PureComponent {
py10: 1,
px10: 1,
})
const iconClasses = CX({
cSecondary: !active,
cWhite: active,
mr10: 1,
py2: small,
ml10: small,
})
const iconSize = !small ? '18px' : '16px'
const textColor = !active ? 'primary' : 'white'
let backgroundColor, color
if (disabledButton) {
backgroundColor = 'tertiary'
color = 'tertiary'
} else if (type === 'navigation') {
backgroundColor = 'white'
color = 'brand'
} else {
backgroundColor = 'brand'
color = 'white'
}
return (
<div className={containerClasses}>
<div className={[_s.d, _s.w100PC, _s.py10, _s.px10].join(' ')}>
<div className={[_s.d, _s.w100PC].join(' ')}>
<Button
isBlock
radiusSmall
isDisabled={disabledButton}
backgroundColor={disabledButton ? 'secondary' : 'brand'}
color={disabledButton ? 'tertiary' : 'white'}
backgroundColor={backgroundColor}
color={color}
className={[_s.fs15PX, _s.px15, _s.flexGrow1, _s.mlAuto].join(' ')}
onClick={this.handleSubmit}
>
<Text color='inherit' weight='medium' align='center'>
post
<Text color='inherit' size='medium' weight='bold' align='center'>
{intl.formatMessage(scheduledAt ? messages.schedulePost : edit ? messages.postEdit : messages.post)}
</Text>
</Button>
</div>
@ -71,9 +123,32 @@ class ComposeFormSubmitButton extends React.PureComponent {
}
// {intl.formatMessage(scheduledAt ? messages.schedulePost : edit ? messages.postEdit : messages.post)}
const messages = defineMessages({
post: { id: 'compose_form.post', defaultMessage: 'Post' },
postEdit: { id: 'compose_form.post_edit', defaultMessage: 'Post Edit' },
schedulePost: { id: 'compose_form.schedule_post', defaultMessage: 'Schedule Post' },
})
const mapStateToProps = (state) => ({
edit: state.getIn(['compose', 'id']) !== null,
text: state.getIn(['compose', 'text']),
isSubmitting: state.getIn(['compose', 'is_submitting']),
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
quoteOfId: state.getIn(['compose', 'quote_of_id']),
scheduledAt: state.getIn(['compose', 'scheduled_at']),
hasPoll: state.getIn(['compose', 'poll']),
})
const mapDispatchToProps = (dispatch) => ({
onSubmit(groupId, replyToId = null, router, isStandalone, autoJoinGroup) {
dispatch(submitCompose(groupId, replyToId, router, isStandalone, autoJoinGroup))
}
})
ComposeFormSubmitButton.propTypes = {
type: PropTypes.oneOf(['header', 'block', 'comment'])
type: PropTypes.oneOf(['header', 'navigation', 'block', 'comment'])
}
export default ComposeFormSubmitButton
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ComposeFormSubmitButton))

@ -48,7 +48,7 @@ class ExpiresPostButton extends React.PureComponent {
}
const messages = defineMessages({
expires: { id: 'expiration.title', defaultMessage: 'Add status expiration' },
expires: { id: 'expiration.title', defaultMessage: 'Status expiration' },
})
const mapStateToProps = (state) => ({

@ -19,7 +19,7 @@ class Upload extends ImmutablePureComponent {
}
state = {
hovered: false,
hovering: false,
focused: false,
dirtyDescription: null,
}
@ -45,11 +45,11 @@ class Upload extends ImmutablePureComponent {
}
handleMouseEnter = () => {
this.setState({ hovered: true })
this.setState({ hovering: true })
}
handleMouseLeave = () => {
this.setState({ hovered: false })
this.setState({ hovering: false })
}
handleInputFocus = () => {
@ -75,66 +75,60 @@ class Upload extends ImmutablePureComponent {
render() {
const { intl, media } = this.props
const active = this.state.hovered || this.state.focused
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
const { hovering } = this.state
const descriptionContainerClasses = CX({
d: 1,
posAbs: 1,
right0: 1,
bottom0: 1,
left0: 1,
mt5: 1,
mb5: 1,
ml5: 1,
mr5: 1,
displayNone: !active,
})
const active = hovering || this.state.focused
const description = this.state.dirtyDescription || (this.state.dirtyDescription !== '' && media.get('description')) || ''
return (
<div
tabIndex='0'
className={[_s.d, _s.w50PC, _s.px5, _s.py5].join(' ')}
className={[_s.d, _s.w100PC, _s.mt10].join(' ')}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onClick={this.handleClick}
role='button'
>
<div className={[_s.d, _s.radiusSmall, _s.overflowHidden, _s.h158PX].join(' ')}>
<div className={[_s.d, _s.radiusSmall, _s.borderColorSecondary, _s.border1PX, _s.overflowHidden, _s.maxH100VH, _s.minH106PX].join(' ')}>
<Image
className={[_s.d, _s.h158PX].join(' ')}
className={[_s.d, _s.minH106PX, _s.maxH100VH].join(' ')}
src={media.get('preview_url')}
/>
{ hovering && <div className={[_s.d, _s.posAbs, _s.z2, _s.top0, _s.bottom0, _s.right0, _s.left0, _s.bgBlackOpaquest].join(' ')} /> }
{
media.get('type') === 'gifv' &&
<div className={[_s.d, _s.posAbs, _s.z2, _s.radiusSmall, _s.bgBlackOpaque, _s.px5, _s.py5, _s.ml10, _s.mt10, _s.top0, _s.left0].join(' ')}>
<div className={[_s.d, _s.posAbs, _s.z3, _s.radiusSmall, _s.bgBlackOpaque, _s.px5, _s.py5, _s.ml10, _s.mt10, _s.bottom0, _s.right0].join(' ')}>
<Text size='extraSmall' color='white' weight='medium'>GIF</Text>
</div>
}
<Button
backgroundColor='black'
color='white'
title={intl.formatMessage(messages.delete)}
onClick={this.handleUndoClick}
icon='close'
iconSize='10px'
iconClassName={_s.inherit}
className={[_s.top0, _s.right0, _s.posAbs, _s.mr5, _s.mt5, _s.px10].join(' ')}
/>
<div className={descriptionContainerClasses}>
<Input
small
hideLabel
id={`input-${media.get('id')}`}
title={intl.formatMessage(messages.description)}
placeholder={intl.formatMessage(messages.description)}
value={description}
maxLength={420}
onFocus={this.handleInputFocus}
onChange={this.handleInputChange}
onBlur={this.handleInputBlur}
onKeyDown={this.handleKeyDown}
<div className={[_s.d, _s.posAbs, _s.px15, _s.pt15, _s.z3, _s.flexRow, _s.top0, _s.left0, _s.right0].join(' ')}>
{
active &&
<div className={[_s.d, _s.flexGrow1, _s.mr15].join(' ')}>
<Input
small
hideLabel
id={`input-${media.get('id')}`}
title={intl.formatMessage(messages.description)}
placeholder={intl.formatMessage(messages.description)}
value={description}
maxLength={420}
onFocus={this.handleInputFocus}
onChange={this.handleInputChange}
onBlur={this.handleInputBlur}
onKeyDown={this.handleKeyDown}
/>
</div>
}
<Button
backgroundColor='black'
color='white'
title={intl.formatMessage(messages.delete)}
onClick={this.handleUndoClick}
icon='close'
iconSize='10px'
iconClassName={_s.inherit}
className={[_s.mlAuto, _s.px10].join(' ')}
/>
</div>
</div>

@ -11,7 +11,7 @@ class SensitiveMediaButton extends React.PureComponent {
const { active, disabled, onClick, intl } = this.props
return (
<div className={[_s.d, _s.aiStart, _s.px5].join(' ')}>
<div className={[_s.d, _s.aiStart, _s.px5, _s.py10].join(' ')}>
<Switch
id='mark-sensitive'
type='checkbox'

@ -18,23 +18,16 @@ class UploadForm extends ImmutablePureComponent {
return (
<div className={_s.d}>
{ isUploading && <ProgressBar small progress={uploadProgress} /> }
<div className={[_s.d, _s.flexRow, _s.flexWrap].join(' ')}>
{
mediaIds.map(id => (
<Upload id={id} key={id} />
))
}
{mediaIds.map(id => (
<Upload id={id} key={id} />
))}
</div>
{
!mediaIds.isEmpty() &&
<SensitiveMediaButton />
}
{
isUploading &&
<ProgressBar small progress={uploadProgress} />
}
{ !mediaIds.isEmpty() && <SensitiveMediaButton /> }
{ isUploading && <ProgressBar small progress={uploadProgress} /> }
</div>
)
}
@ -48,7 +41,6 @@ const mapStateToProps = (state) => ({
})
UploadForm.propTypes = {
isModalOpen: PropTypes.bool,
isUploading: PropTypes.bool,
mediaIds: ImmutablePropTypes.list.isRequired,
uploadProgress: PropTypes.number,

@ -11,7 +11,7 @@ class Compose extends React.PureComponent {
}
render () {
return <ComposeFormContainer isStandalone />
return <ComposeFormContainer formLocation='standalone' />
}
}

@ -186,9 +186,9 @@ class SlideFirstPost extends React.PureComponent {
<div className={[_s.d, _s.mt15, _s.boxShadowBlock, _s.radiusSmall].join(' ')}>
<ComposeFormContainer
formLocation='introduction'
groupId={GAB_COM_INTRODUCE_YOURSELF_GROUP_ID}
hidePro
autoFocus
autoJoinGroup
/>
</div>
@ -325,7 +325,7 @@ class Introduction extends ImmutablePureComponent {
<Button
href={currentIndex === 3 ? '/home' : undefined}
onClick={this.handleNext}
className={_s.px10}
className={[_s.px10, _s.aiCenter, _s.flexRow].join(' ')}
icon={currentIndex !== 3 ? 'arrow-right' : undefined}
iconSize={currentIndex !== 3 ? '18px' : undefined}
>
@ -336,7 +336,7 @@ class Introduction extends ImmutablePureComponent {
<Text color='white' className={_s.px5}>{nextTitle}</Text>
</Responsive>
<Responsive max={BREAKPOINT_EXTRA_SMALL}>
<Text color='white' className={[_s.px5, _s.mr10].join(' ')}>Done</Text>
<Text color='white' className={[_s.px5, _s.mr5].join(' ')}>Done</Text>
<Icon id='check' size='14px' className={_s.cWhite} />
</Responsive>
</React.Fragment>

@ -35,16 +35,17 @@ class ChatConversationsListItem extends ImmutablePureComponent {
if (!chatConversation) return <div/>
console.log("chatConversation:", chatConversation)
const containerClasses = CX({
d: 1,
w100PC: 1,
bgTransparent: 1,
bgSubtle_onHover: 1,
borderBottom1PX: 1,
borderColorSecondary: 1,
noUnderline: 1,
outlineNone: 1,
cursorPointer: 1,
pl15: 1,
})
const innerContainerClasses = CX({
@ -71,6 +72,9 @@ class ChatConversationsListItem extends ImmutablePureComponent {
className={containerClasses}
onClick={this.handleOnClick}
>
{ chatConversation.get('is_unread') && <div className={[_s.d, _s.posAbs, _s.left0, _s.top50PC, _s.ml10, _s.mtNeg5PX, _s.circle, _s.w10PX, _s.h10PX, _s.bgBrand].join(' ')} /> }
<div className={innerContainerClasses}>
<AvatarGroup accounts={otherAccounts} size={avatarSize} noHover />
@ -88,6 +92,7 @@ class ChatConversationsListItem extends ImmutablePureComponent {
<div className={[_s.py5, _s.dangerousContent, _s.textAlignLeft].join(' ')} dangerouslySetInnerHTML={content} />
</div>
<div className={[_s.d, _s.posAbs, _s.h1PX, _s.w100PC, _s.bottom0, _s.right0, _s.bgSecondary].join(' ')} />
</div>
</button>
)

@ -8,6 +8,7 @@ import { openModal } from '../../../actions/modal'
import { sendChatMessage } from '../../../actions/chat_messages'
import { CX } from '../../../constants'
import Button from '../../../components/button'
import Icon from '../../../components/icon'
import Input from '../../../components/input'
import Text from '../../../components/text'
@ -23,6 +24,10 @@ class ChatMessagesComposeForm extends React.PureComponent {
this.setState({ value: '' })
}
handleOnExpire = () => {
//
}
onChange = (e) => {
this.setState({ value: e.target.value })
}
@ -68,6 +73,10 @@ class ChatMessagesComposeForm extends React.PureComponent {
this.sendBtn = c
}
setExpiresBtn = (c) => {
this.expiresBtn = c
}
render () {
const { isXS, chatConversationId } = this.props
const { value } = this.state
@ -85,9 +94,7 @@ class ChatMessagesComposeForm extends React.PureComponent {
px10: 1,
fs14PX: 1,
maxH200PX: 1,
borderColorSecondary: 1,
border1PX: 1,
radiusRounded: 1,
w100PC: 1,
py10: 1,
})
@ -105,6 +112,7 @@ class ChatMessagesComposeForm extends React.PureComponent {
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
aria-autocomplete='list'
maxLength={1600}
/>
)
@ -114,18 +122,33 @@ class ChatMessagesComposeForm extends React.PureComponent {
disabled={disabled}
onClick={this.handleOnSendChatMessage}
>
<Text color='inherit' weight='medium' className={_s.px10}>Send</Text>
<Text color='inherit' weight='medium' className={isXS ? undefined : _s.px10}>Send</Text>
</Button>
)
const expiresBtn = (
<button
ref={this.setExpiresBtn}
className={[_s.d, _s.bgSubtle, _s.borderRight1PX, _s.borderColorSecondary, _s.w40PX, _s.h100PC, _s.aiCenter, _s.jcCenter, _s.cursorPointer, _s.outlineNone].join(' ')}
onClick={this.handleOnExpire}
>
<Icon id='stopwatch' className={[_s.cPrimary, _s.ml2].join(' ')} size='15px' />
</button>
)
if (isXS) {
return (
<div className={[_s.d, _s.z4, _s.minH58PX, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.minH58PX, _s.bgPrimary, _s.aiCenter, _s.z3, _s.bottom0, _s.right0, _s.left0, _s.posFixed].join(' ')} >
<div className={[_s.d, _s.w100PC, _s.pb5, _s.px15, _s.aiCenter, _s.jcCenter, _s.saveAreaInsetPB, _s.saveAreaInsetPL, _s.saveAreaInsetPR, _s.w100PC].join(' ')}>
<div className={[_s.d, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px10].join(' ')}>
<div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].join(' ')}>
{textarea}
<div className={[_s.d, _s.flexRow, _s.radiusRounded, _s.border1PX, _s.borderColorSecondary, _s.overflowHidden].join(' ')}>
<div className={_s.d}>
{expiresBtn}
</div>
<div className={[_s.d, _s.flexGrow1].join(' ')}>
{textarea}
</div>
</div>
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
{button}
@ -140,9 +163,16 @@ class ChatMessagesComposeForm extends React.PureComponent {
return (
<div className={[_s.d, _s.posAbs, _s.bottom0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.bgPrimary, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px15].join(' ')}>
<div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].join(' ')}>
{textarea}
<div className={[_s.d, _s.flexRow, _s.radiusRounded, _s.border1PX, _s.borderColorSecondary, _s.overflowHidden].join(' ')}>
<div className={_s.d}>
{expiresBtn}
</div>
<div className={[_s.d, _s.flexGrow1].join(' ')}>
{textarea}
</div>
</div>
</div>
<div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
<div className={[_s.d, _s.h100PC, _s.mtAuto, _s.mb10, _s.aiCenter, _s.jcCenter].join(' ')}>
{button}
</div>
</div>
@ -163,4 +193,4 @@ ChatMessagesComposeForm.propTypes = {
onSendMessage: PropTypes.func.isRequired,
}
export default connect(null, mapDispatchToProps)(ChatMessagesComposeForm)
export default connect(mapDispatchToProps)(ChatMessagesComposeForm)

@ -8,7 +8,7 @@ import { NavLink } from 'react-router-dom'
import { openPopover } from '../../../actions/popover'
import {
CX,
POPOVER_CHAT_MESSAGE_DELETE,
POPOVER_CHAT_MESSAGE_OPTIONS,
} from '../../../constants'
import { me } from '../../../initial_state'
import Input from '../../../components/input'
@ -51,7 +51,7 @@ class ChatMessageItem extends ImmutablePureComponent {
}
handleMoreClick = () => {
this.props.onOpenChatMessageDeletePopover(this.props.chatMessageId, this.deleteBtnRef)
this.props.onOpenChatMessageOptionsPopover(this.props.chatMessageId, this.deleteBtnRef)
}
setDeleteBtnRef = (c) => {
@ -122,7 +122,7 @@ class ChatMessageItem extends ImmutablePureComponent {
const buttonContainerClasses = CX({
d: 1,
flexRow: 1,
displayNone: !isHovering && alt,
displayNone: !isHovering,
})
return (
@ -145,19 +145,16 @@ class ChatMessageItem extends ImmutablePureComponent {
<div className={messageInnerContainerClasses}>
<div className={[_s.py5, _s.dangerousContent, _s.cPrimary].join(' ')} dangerouslySetInnerHTML={content} />
</div>
{
alt &&
<div className={buttonContainerClasses}>
<Button
buttonRef={this.setDeleteBtnRef}
onClick={this.handleMoreClick}
color='tertiary'
backgroundColor='none'
icon='ellipsis'
iconSize='18px'
/>
</div>
}
<div className={buttonContainerClasses}>
<Button
buttonRef={this.setDeleteBtnRef}
onClick={this.handleMoreClick}
color='tertiary'
backgroundColor='none'
icon='ellipsis'
iconSize='18px'
/>
</div>
</div>
<div className={lowerContainerClasses}>
<Text size='extraSmall' color='tertiary' align={alt ? 'right' : 'left'}>
@ -178,8 +175,8 @@ const mapStateToProps = (state, { lastChatMessageId, chatMessageId }) => ({
})
const mapDispatchToProps = (dispatch) => ({
onOpenChatMessageDeletePopover(chatMessageId, targetRef) {
dispatch(openPopover(POPOVER_CHAT_MESSAGE_DELETE, {
onOpenChatMessageOptionsPopover(chatMessageId, targetRef) {
dispatch(openPopover(POPOVER_CHAT_MESSAGE_OPTIONS, {
targetRef,
chatMessageId,
position: 'top',

@ -15,6 +15,7 @@ import {
expandChatMessages,
scrollBottomChatMessageConversation,
} from '../../../actions/chat_conversation_messages'
import { readChatConversation } from '../../../actions/chat_conversations'
import IntersectionObserverArticle from '../../../components/intersection_observer_article'
import IntersectionObserverWrapper from '../../ui/util/intersection_observer_wrapper'
import ChatMessagePlaceholder from '../../../components/placeholder/chat_message_placeholder'
@ -58,7 +59,6 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
// Reset the scroll position when a new child comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (snapshot !== null && this.scrollContainerRef) {
console.log("snapshot:", snapshot)
this.setScrollTop(this.scrollContainerRef.scrollHeight - snapshot)
}
@ -68,6 +68,7 @@ class ChatMessageScrollingList extends ImmutablePureComponent {
if (prevProps.chatMessageIds.size === 0 && this.props.chatMessageIds.size > 0 && this.scrollContainerRef) {
this.scrollContainerRef.scrollTop = this.scrollContainerRef.scrollHeight
this.props.onReadChatConversation(this.props.chatConversationId)
}
}
@ -363,6 +364,9 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
onSetChatConversationSelected: (chatConversationId) => {
dispatch(setChatConversationSelected(chatConversationId))
},
onReadChatConversation(chatConversationId) {
dispatch(readChatConversation(chatConversationId))
},
})
ChatMessageScrollingList.propTypes = {

@ -52,11 +52,13 @@ import DeckPage from '../../pages/deck_page'
import {
About,
AccountAlbums,
AccountGallery,
AccountTimeline,
AccountCommentsTimeline,
Assets,
BlockedAccounts,
BookmarkCollections,
BookmarkedStatuses,
CaliforniaConsumerProtection,
CaliforniaConsumerProtectionContact,
@ -274,9 +276,11 @@ class SwitchingArea extends React.PureComponent {
<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/likes' page={ProfilePage} component={LikedStatuses} content={children} />
<WrappedRoute path='/:username/bookmarks' page={ProfilePage} component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/:username/bookmarks' page={ProfilePage} component={BookmarkCollections} content={children} />
<WrappedRoute path='/:username/:bookmarkCollectionId/bookmarks' page={ProfilePage} component={BookmarkedStatuses} content={children} />
<WrappedRoute path='/:username/posts/:statusId' publicRoute exact page={BasicPage} component={StatusFeature} content={children} componentParams={{ title: 'Status', page: 'status' }} />

@ -6,6 +6,7 @@ export function AccountGallery() { return import(/* webpackChunkName: "features/
export function Assets() { return import(/* webpackChunkName: "features/about/assets" */'../../about/assets') }
export function BlockAccountModal() { return import(/* webpackChunkName: "components/block_account_modal" */'../../../components/modal/block_account_modal') }
export function BlockedAccounts() { return import(/* webpackChunkName: "features/blocked_accounts" */'../../blocked_accounts') }
export function BookmarkCollections() { return import(/* webpackChunkName: "features/bookmark_collections" */'../../bookmark_collections') }
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') }
@ -17,17 +18,19 @@ export function ChatConversationDeleteModal() { return import(/* webpackChunkNam
export function ChatConversationMutedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_muted_accounts" */'../../chat_conversation_muted_accounts') }
export function ChatConversationOptionsPopover() { return import(/* webpackChunkName: "components/chat_conversation_options_popover" */'../../../components/popover/chat_conversation_options_popover') }
export function ChatConversationRequests() { return import(/* webpackChunkName: "features/chat_conversation_requests" */'../../chat_conversation_requests') }
export function ChatMessageDeletePopover() { return import(/* webpackChunkName: "components/chat_message_delete_popover" */'../../../components/popover/chat_message_delete_popover') }
export function ChatMessageOptionsPopover() { return import(/* webpackChunkName: "components/chat_message_options_popover" */'../../../components/popover/chat_message_options_popover') }
export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') }
export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') }
export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') }
export function Compose() { return import(/* webpackChunkName: "features/compose" */'../../compose') }
export function ComposeForm() { return import(/* webpackChunkName: "components/compose_form" */'../../compose/components/compose_form') }
export function ComposeModal() { return import(/* webpackChunkName: "components/compose_modal" */'../../../components/modal/compose_modal') }
export function ComposePostDesinationPopover() { return import(/* webpackChunkName: "components/compose_post_destination_popover" */'../../../components/popover/compose_post_destination_popover') }
export function ConfirmationModal() { return import(/* webpackChunkName: "components/confirmation_modal" */'../../../components/modal/confirmation_modal') }
export function DatePickerPopover() { return import(/* webpackChunkName: "components/date_picker_popover" */'../../../components/popover/date_picker_popover') }
export function Deck() { return import(/* webpackChunkName: "features/deck" */'../../deck') }
export function DeckColumnAddModal() { return import(/* webpackChunkName: "components/deck_column_add_modal" */'../../../components/modal/deck_column_add_modal') }
export function DeckColumnAddOptionsModal() { return import(/* webpackChunkName: "components/deck_column_add_options_modal" */'../../../components/modal/deck_column_add_options_modal') }
export function DisplayOptionsModal() { return import(/* webpackChunkName: "components/display_options_modal" */'../../../components/modal/display_options_modal') }
export function DMCA() { return import(/* webpackChunkName: "features/about/dmca" */'../../about/dmca') }
export function EditProfileModal() { return import(/* webpackChunkName: "components/edit_profile_modal" */'../../../components/modal/edit_profile_modal') }

@ -20,6 +20,7 @@ export const isStaff = getMeta('is_staff');
export const unreadCount = getMeta('unread_count');
export const lastReadNotificationId = getMeta('last_read_notification_id');
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
export const trendingHashtags = getMeta('trending_hashtags');
export const isFirstSession = getMeta('is_first_session');
export const emailConfirmed = getMeta('email_confirmed');
export const meEmail = getMeta('email');

@ -5,13 +5,48 @@ import {
BREAKPOINT_EXTRA_SMALL,
} from '../constants'
import { me } from '../initial_state'
import Button from '../components/button'
import Text from '../components/text'
import DeckSidebar from '../components/sidebar/deck_sidebar'
import WrappedBundle from '../features/ui/util/wrapped_bundle'
import { getWindowDimension } from '../utils/is_mobile'
const initialState = getWindowDimension()
class DeckLayout extends React.PureComponent {
state = {
width: initialState.width,
}
componentDidMount() {
this.handleResize()
window.addEventListener('resize', this.handleResize, false)
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false)
}
handleResize = () => {
const { width } = getWindowDimension()
this.setState({ width })
}
render() {
const { children, title } = this.props
const { width } = this.state
const isXS = width <= BREAKPOINT_EXTRA_SMALL
if (isXS) {
return (
<div className={[_s.d, _s.aiCenter, _s.jcCenter, _s.w100PC, _s.h100VH, _s.bgTertiary].join(' ')}>
<Text className={_s.mb10}>Gab Deck is not available on mobile or tablet devices. Please only access using a desktop computer.</Text>
<Button to='/home'>Return home</Button>
</div>
)
}
const mainBlockClasses = CX({
d: 1,

@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { defineMessages, injectIntl } from 'react-intl'
import { me } from '../initial_state'
import { me, trendingHashtags } from '../initial_state'
import {
BREAKPOINT_EXTRA_SMALL,
CX,
@ -42,26 +42,20 @@ class SearchLayout extends React.PureComponent {
title: 'Explore',
onClick: () => this.setState({ currentExploreTabIndex: 0 }),
component: ExploreTimeline,
},
{
title: '#Election2020',
onClick: () => this.setState({ currentExploreTabIndex: 1 }),
component: HashtagTimeline,
componentParams: { params: { id: 'election2020' } },
},
{
title: '#RiggedElection',
onClick: () => this.setState({ currentExploreTabIndex: 2 }),
component: HashtagTimeline,
componentParams: { params: { id: 'riggedelection' } },
},
{
title: '#StopTheSteal',
onClick: () => this.setState({ currentExploreTabIndex: 3 }),
component: HashtagTimeline,
componentParams: { params: { id: 'stopthesteal' } },
},
}
]
if (Array.isArray(trendingHashtags)) {
trendingHashtags.forEach((tag, i) => {
let j = i + 1
this.exploreTabs.push({
title: `#${tag}`,
onClick: () => this.setState({ currentExploreTabIndex: j }),
component: HashtagTimeline,
componentParams: { params: { id: `${tag}`.toLowerCase() } },
})
})
}
this.searchTabs = [
{

@ -0,0 +1,62 @@
import {
BOOKMARK_COLLECTIONS_FETCH_REQUEST,
BOOKMARK_COLLECTIONS_FETCH_SUCCESS,
BOOKMARK_COLLECTIONS_FETCH_FAIL,
} from '../actions/bookmarks'
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
isFetched: false,
isError: false,
})
const normalizeBookmarkCollection = (bookmarkCollection) => {
return {
id: shortcut.id,
shortcut_type: 'account',
shortcut_id: shortcut.shortcut_id,
title: shortcut.shortcut.acct,
image: shortcut.shortcut.avatar_static,
to: `/${shortcut.shortcut.acct}`,
}
}
const normalizeBookmarkCollections = (shortcuts) => {
return fromJS(shortcuts.map((shortcut) => {
return normalizeShortcut(shortcut)
}))
}
export default function albums(state = initialState, action) {
switch(action.type) {
case SHORTCUTS_FETCH_REQUEST:
return state.withMutations((map) => {
map.set('isLoading', true)
map.set('isFetched', false)
map.set('isError', false)
})
case SHORTCUTS_FETCH_SUCCESS:
return state.withMutations((map) => {
map.set('items', normalizeShortcuts(action.shortcuts))
map.set('isLoading', false)
map.set('isFetched', true)
map.set('isError', false)
})
case SHORTCUTS_FETCH_FAIL:
return state.withMutations((map) => {
map.set('isLoading', false)
map.set('isFetched', true)
map.set('isError', true)
})
case BOOKMARK_COLLECTIONS_CREATE_REQUEST:
return state.update('items', list => list.push(fromJS(normalizeShortcut(action.shortcut))))
case BOOKMARK_COLLECTIONS_REMOVE_REQUEST:
return state.update('items', list => list.filterNot((item) => {
return `${item.get('id')}` === `${action.shortcutId}`
}))
default:
return state
}
}

@ -0,0 +1,62 @@
import {
BOOKMARK_COLLECTIONS_FETCH_REQUEST,
BOOKMARK_COLLECTIONS_FETCH_SUCCESS,
BOOKMARK_COLLECTIONS_FETCH_FAIL,
} from '../actions/bookmarks'
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
isFetched: false,
isError: false,
})
const normalizeBookmarkCollection = (bookmarkCollection) => {
return {
id: shortcut.id,
shortcut_type: 'account',
shortcut_id: shortcut.shortcut_id,
title: shortcut.shortcut.acct,
image: shortcut.shortcut.avatar_static,
to: `/${shortcut.shortcut.acct}`,
}
}
const normalizeBookmarkCollections = (shortcuts) => {
return fromJS(shortcuts.map((shortcut) => {
return normalizeShortcut(shortcut)
}))
}
export default function bookmark_collections(state = initialState, action) {
switch(action.type) {
case SHORTCUTS_FETCH_REQUEST:
return state.withMutations((map) => {
map.set('isLoading', true)
map.set('isFetched', false)
map.set('isError', false)
})
case SHORTCUTS_FETCH_SUCCESS:
return state.withMutations((map) => {
map.set('items', normalizeShortcuts(action.shortcuts))
map.set('isLoading', false)
map.set('isFetched', true)
map.set('isError', false)
})
case SHORTCUTS_FETCH_FAIL:
return state.withMutations((map) => {
map.set('isLoading', false)
map.set('isFetched', true)
map.set('isError', true)
})
case BOOKMARK_COLLECTIONS_CREATE_REQUEST:
return state.update('items', list => list.push(fromJS(normalizeShortcut(action.shortcut))))
case BOOKMARK_COLLECTIONS_REMOVE_REQUEST:
return state.update('items', list => list.filterNot((item) => {
return `${item.get('id')}` === `${action.shortcutId}`
}))
default:
return state
}
}

@ -7,6 +7,7 @@ import { me } from '../initial_state'
import {
CHAT_MESSAGES_SEND_SUCCESS,
CHAT_MESSAGES_DELETE_REQUEST,
CHAT_MESSAGES_PURGE_REQUEST,
} from '../actions/chat_messages'
import {
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
@ -14,6 +15,7 @@ import {
CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS,
CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS,
CHAT_CONVERSATION_REQUEST_APPROVE_SUCCESS,
CHAT_CONVERSATION_MARK_READ_SUCCESS,
} from '../actions/chat_conversations'
const initialState = ImmutableMap()
@ -50,6 +52,11 @@ export default function chat_conversations(state = initialState, action) {
case CHAT_MESSAGES_DELETE_REQUEST:
// : todo : set last conversation message to one prior to this one
return state
case CHAT_MESSAGES_PURGE_REQUEST:
// : todo :
return state
case CHAT_CONVERSATION_MARK_READ_SUCCESS:
return importChatConversation(state, action.chatConversation)
default:
return state
}

@ -2,6 +2,7 @@ import { Map as ImmutableMap, fromJS } from 'immutable'
import {
CHAT_MESSAGES_SEND_SUCCESS,
CHAT_MESSAGES_DELETE_REQUEST,
CHAT_MESSAGES_PURGE_REQUEST,
} from '../actions/chat_messages'
import {
CHAT_MESSAGES_IMPORT,
@ -26,6 +27,8 @@ export default function chat_messages(state = initialState, action) {
return importChatMessage(state, action.chatMessage)
case CHAT_MESSAGES_DELETE_REQUEST:
return deleteChatMessage(state, action.chatMessageId)
case CHAT_MESSAGES_PURGE_REQUEST:
return state
default:
return state
}

@ -11,6 +11,7 @@ import {
import {
CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS,
CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS,
CHAT_CONVERSATION_MARK_READ_FETCH,
} from '../actions/chat_conversations'
import {
CHAT_MESSAGES_FETCH_SUCCESS,
@ -34,6 +35,10 @@ export default function chats(state = initialState, action) {
return state.set('chatConversationRequestCount', action.count)
case CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS:
return state.set('chatsUnreadCount', action.count)
case CHAT_CONVERSATION_MARK_READ_FETCH:
const chatConversationUnreadCount = action.chatConversation.get('unread_count')
const totalUnreadCount = state.get('chatsUnreadCount')
return state.set('chatsUnreadCount', Math.max(totalUnreadCount - chatConversationUnreadCount, 0))
default:
return state
}

@ -360,6 +360,7 @@ pre {
.circle { border-radius: var(--radius-circle); }
.radiusSmall { border-radius: var(--radius-small); }
.radiusRounded { border-radius: var(--radius-rounded); }
.topLeftRadiusSmall { border-top-left-radius: var(--radius-small); }
.topRightRadiusSmall { border-top-right-radius: var(--radius-small); }
.bottomRightRadiusSmall { border-bottom-right-radius: var(--radius-small); }
@ -443,6 +444,7 @@ pre {
.bgBlack { background-color: var(--color_black); }
.bgBlackOpaque { background-color: var(--color_black-opaquer); }
.bgBlackOpaquest { background-color: var(--color_black-opaquest); }
.bgBlackOpaque_onHover:hover { background-color: var(--color_black-opaque); }
.bgBlackOpaquest_onHover:hover { background-color: var(--color_black-opaquest); }
@ -550,10 +552,9 @@ pre {
.calcH53PX { height: calc(100vh - 53px); }
.calcH80VH106PX { height: calc(80vh - 106px); }
.calcMaxH370PX { max-height: calc(100vh - 370px); }
@media (min-height: 0px) and (max-height:660px) {
.calcMaxH370PX { max-height: calc(100vh - 140px); }
}
.calcMaxH410PX { max-height: calc(100vh - 450px); }
@media (min-width: 0px) and (max-width:992) { .calcMaxH410PX { max-height: calc(100vh - 410px); } }
@media (min-height: 0px) and (max-height:660px) { .calcMaxH410PX { max-height: calc(100vh - 140px); } }
.minH100VH { min-height: 100vh; }
.minH50VH { min-height: 50vh; }
@ -841,6 +842,7 @@ pre {
.mt5 { margin-top: 5px; }
.mt2 { margin-top: 2px; }
.mtAuto { margin-top: auto; }
.mtNeg5PX { margin-top: -5px; }
.mtNeg26PX { margin-top: -26px; }
.mtNeg32PX { margin-top: -32px; }
.mtNeg50PX { margin-top: -50px; }
@ -872,6 +874,7 @@ pre {
.pl0 { padding-left: 0; }
.pr50 { padding-right: 50px; }
.pr20 { padding-right: 20px; }
.pr15 { padding-right: 15px; }
.pr10 { padding-right: 10px; }
.pr5 { padding-right: 5px; }

@ -132,7 +132,7 @@ class FeedManager
private
def push_update_required?(timeline_id)
redis.exists("subscribed:#{timeline_id}")
redis.exists?("subscribed:#{timeline_id}")
end
def blocks_or_mutes?(receiver_id, account_ids, context)

@ -50,6 +50,7 @@
# is_verified :boolean default(FALSE), not null
# is_donor :boolean default(FALSE), not null
# is_investor :boolean default(FALSE), not null
# is_flagged_as_spam :boolean default(FALSE), not null
#
class Account < ApplicationRecord
@ -91,7 +92,7 @@ class Account < ApplicationRecord
scope :recent, -> { reorder(id: :desc) }
scope :bots, -> { where(actor_type: %w(Application Service)) }
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
scope :by_domain_accounts, -> { group(:domain).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
scope :by_domain_accounts, -> { group(:id).select(:domain, 'COUNT(*) AS accounts_count').order('accounts_count desc') }
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
@ -148,6 +149,14 @@ class Account < ApplicationRecord
Follow.where(target_account_id: id).count
end
def chat_conversation_accounts_count
ChatConversationAccount.where(account_id: id).count
end
def chat_messages_count
ChatMessage.where(from_account_id: id).count
end
def silenced?
silenced_at.present?
end

@ -101,7 +101,7 @@ class AccountConversation < ApplicationRecord
end
def subscribed_to_timeline?
Redis.current.exists("subscribed:#{streaming_channel}")
Redis.current.exists?("subscribed:#{streaming_channel}")
end
def streaming_channel

@ -17,15 +17,26 @@
#
# : todo : expires
# : todo : max per account
class ChatConversationAccount < ApplicationRecord
include Paginable
PER_ACCOUNT_APPROVED_LIMIT = 100
EXPIRATION_POLICY_MAP = {
none: nil,
five_minutes: '1',
sixty_minutes: '2',
six_hours: '3',
one_day: '4',
three_days: '5',
one_week: '6',
}.freeze
belongs_to :account
belongs_to :chat_conversation
belongs_to :last_chat_message, class_name: 'ChatMessage', optional: true
# before_validation :set_last_chat_message
def participant_accounts
if participant_account_ids.empty?
[account]
@ -35,10 +46,4 @@ class ChatConversationAccount < ApplicationRecord
end
end
private
def set_last_chat_message
self.last_chat_message_id = nil # : todo :
end
end

@ -8,7 +8,7 @@ class HomeFeed < Feed
end
def get(limit, max_id = nil, since_id = nil, min_id = nil)
if redis.exists("account:#{@account.id}:regeneration")
if redis.exists?("account:#{@account.id}:regeneration")
from_database(limit, max_id, since_id, min_id)
else
super
@ -18,6 +18,7 @@ class HomeFeed < Feed
private
def from_database(limit, max_id, since_id, min_id)
puts "tilly from_database"
Status.as_home_timeline(@account)
.paginate_by_id(limit, max_id: max_id, since_id: since_id, min_id: min_id)
.reject { |status| FeedManager.instance.filter?(:home, status, @account.id) }

@ -18,9 +18,13 @@ class LinkBlock < ApplicationRecord
return false if text.nil?
return false if text.length < 1
urls = text.scan(FetchLinkCardService::URL_PATTERN).map { |array| Addressable::URI.parse(array[0]).normalize }
urls = text.scan(FetchLinkCardService::URL_PATTERN).map {|array|
Addressable::URI.parse(array[0]).normalize
}
url = urls.first
link_for_fetch = TagManager.instance.normalize_link(url)
where(link: link_for_fetch).exists?
link_for_fetch = link_for_fetch.chomp("/")
where("LOWER(link) LIKE LOWER(?)", "%#{link_for_fetch}%").exists?
end
end

@ -3,22 +3,23 @@
#
# Table name: media_attachments
#
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
# account_id :bigint(8)
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# file_file_name :string
# file_content_type :string
# file_file_size :integer
# file_updated_at :datetime
# remote_url :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# shortcode :string
# type :integer default("image"), not null
# file_meta :json
# account_id :bigint(8)
# description :text
# scheduled_status_id :bigint(8)
# blurhash :string
# media_attachment_album_id :bigint(8)
#
class MediaAttachment < ApplicationRecord

@ -0,0 +1,25 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: media_attachment_albums
#
# id :bigint(8) not null, primary key
# title :text default(""), not null
# description :text
# account_id :integer not null
# visibility :integer default("public"), not null
# created_at :datetime not null
# updated_at :datetime not null
# cover_id :bigint(8)
#
class MediaAttachmentAlbum < ApplicationRecord
enum visibility: [
:public,
:private,
], _suffix: :visibility
belongs_to :account
end

@ -290,13 +290,13 @@ class Status < ApplicationRecord
end
def as_home_timeline(account)
query = where('created_at > ?', 5.days.ago)
query = where('created_at > ?', 10.days.ago)
query.where(visibility: [:public, :unlisted, :private])
query.where(account: [account] + account.following).without_replies
end
def as_group_timeline(group)
query = where('created_at > ?', 5.days.ago)
query = where('created_at > ?', 10.days.ago)
query.where(group: group).without_replies
end

@ -3,11 +3,12 @@
#
# Table name: status_bookmarks
#
# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint(8) not null
# status_id :bigint(8) not null
# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint(8) not null
# status_id :bigint(8) not null
# status_bookmark_collection_id :bigint(8)
#
class StatusBookmark < ApplicationRecord

@ -0,0 +1,19 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: status_bookmark_collections
#
# id :bigint(8) not null, primary key
# title :text default(""), not null
# account_id :integer not null
# created_at :datetime not null
# updated_at :datetime not null
#
class StatusBookmarkCollection < ApplicationRecord
PER_ACCOUNT_LIMIT = 100
belongs_to :account
end

@ -31,6 +31,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:unread_count] = unread_count object.current_account
store[:last_read_notification_id] = object.current_account.user.last_read_notification
store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_amount") || 0
store[:trending_hashtags] = get_trending_hashtags
store[:is_first_session] = is_first_session object.current_account
store[:email_confirmed] = object.current_account.user.confirmed?
store[:email] = object.current_account.user.confirmed? ? '[hidden]' : object.current_account.user.email
@ -39,6 +40,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store
end
def compose
store = {}
@ -78,4 +80,9 @@ class InitialStateSerializer < ActiveModel::Serializer
object.current_account.user.sign_in_count === 1
end
def get_trending_hashtags
tags = Redis.current.get("admin_trending_hashtags") || ""
return tags.strip.split(", ")
end
end

@ -4,7 +4,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
:note, :url, :avatar, :avatar_static, :header, :header_static,
:note, :url, :avatar, :avatar_static, :header, :header_static, :is_flagged_as_spam,
:followers_count, :following_count, :statuses_count, :is_pro, :is_verified, :is_donor, :is_investor
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?

@ -1,7 +1,9 @@
# frozen_string_literal: true
class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
attributes :id, :is_hidden, :is_approved, :unread_count, :is_unread, :chat_conversation_id, :created_at
attributes :id, :is_hidden, :is_approved, :unread_count,
:is_unread, :chat_conversation_id, :created_at,
:is_blocked, :is_muted, :chat_message_expiration_policy
has_many :participant_accounts, key: :other_accounts, serializer: REST::AccountSerializer
has_one :last_chat_message, serializer: REST::ChatMessageSerializer, unless: :last_chat_message_id?
@ -22,4 +24,12 @@ class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
object.unread_count > 0
end
def is_blocked
false
end
def is_muted
false
end
end

@ -2,7 +2,7 @@
class REST::ChatMessageSerializer < ActiveModel::Serializer
attributes :id, :text_html, :text, :language, :from_account_id,
:chat_conversation_id, :created_at
:chat_conversation_id, :created_at, :expires_at
def id
object.id.to_s

@ -0,0 +1,12 @@
# frozen_string_literal: true
class DeleteChatMessageService < BaseService
def call(account, chatMessageId)
@chat = ChatMessage.where(from_account: account).find(chatMessageId)
# : todo :
# make sure last_chat_message_id in chat_account_conversation gets set to last
@chat.destroy!
end
end

@ -6,12 +6,7 @@ class FanOutOnWriteService < BaseService
# @param [Status] status
def call(status)
raise GabSocial::RaceConditionError if status.visibility.nil?
if status.direct_visibility? || status.limited_visibility?
#
else
deliver_to_self(status) if status.account.local?
end
deliver_to_self(status) if status.account.local?
end
private

@ -0,0 +1,104 @@
# frozen_string_literal: true
class PostChatMessageService < BaseService
def call(account, options = {})
@account = account
@options = options
@text = @options[:text] || ''
@chat_conversation = @options[:chat_conversation]
preprocess_attributes!
validate_text!
validate_links!
set_chat_conversation_recipients!
set_message_expiration_date!
process_chat!
postprocess_chat!
@chat
end
def preprocess_attributes!
@text = ActionController::Base.helpers.strip_tags(@text)
unless @chat_conversation
raise ActiveRecord::RecordInvalid
end
rescue ArgumentError
raise ActiveRecord::RecordInvalid
end
def validate_links!
raise GabSocial::NotPermittedError if LinkBlock.block?(@text)
end
def validate_text!
raise GabSocial::NotPermittedError if @text.nil? || @text.strip.length == 0
end
def process_chat!
@chat = ChatMessage.create!(
from_account: @account,
chat_conversation: @chat_conversation,
text: @text
expires_at: @expires_at
)
end
def postprocess_chat!
@chat_conversation_recipients_accounts = ChatConversationAccount.where(chat_conversation: @chat_conversation)
@chat_conversation_recipients_accounts.each do |recipient|
recipient.last_chat_message_id = @chat.id
recipient.is_hidden = false # reset to show unless blocked
# Get not mine
if @account_conversation.id != recipient.id
recipient.unread_count = recipient.unread_count + 1
# : todo :
# check if muting, redis
payload = InlineRenderer.render(@chat, recipient.account, :chat_message)
Redis.current.publish("chat_messages:#{recipient.account.id}", Oj.dump(event: :notification, payload: payload))
else
recipient.unread_count = 0
end
recipient.save
end
end
def set_chat_conversation_recipients!
# : todo :
# check if chat blocked
# check if normal blocked
@account_conversation = ChatConversationAccount.where(account: @account, chat_conversation: @chat_conversation).first
rescue ArgumentError
raise ActiveRecord::RecordInvalid
end
def set_message_expiration_date
case @account_conversation.expiration_policy
when :five_minutes
@expires_at = 5.minutes
when :sixty_minutes
@expires_at = 1.hour
when :six_hours
@expires_at = 6.hours
when :one_day
@expires_at = 1.day
when :three_days
@expires_at = 3.days
when :one_week
@expires_at = 1.week
else
@expires_at = nil
end
@expires_at
end
end

@ -0,0 +1,28 @@
# frozen_string_literal: true
class PurgeChatMessagesService < BaseService
def call(account, chat_conversation)
unless account.is_pro
raise GabSocial::NotPermittedError
end
# Destroy all
ChatMessage.where(from_account: account, chat_conversation: chat_conversation).in_batches.destroy_all
@last_chat_in_conversation = ChatMessage.where(chat_conversation: chat_conversation).first
@chat_conversation_recipients_accounts = ChatConversationAccount.where(chat_conversation: chat_conversation)
@chat_conversation_recipients_accounts.each do |recipient|
# make sure last_chat_message_id in chat_account_conversation gets set to last
unless @last_chat_in_conversation.nil?
recipient.last_chat_message_id = @last_chat_in_conversation.id
else
recipient.last_chat_message_id = nil
end
# Reset and save
recipient.unread_count = 0
recipient.save
end
end
end

Some files were not shown because too many files have changed in this diff Show More