aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2018-10-19 01:47:29 +0200
committerGitHub <noreply@github.com>2018-10-19 01:47:29 +0200
commita38a452481d0f5207bb27ba7a2707c0028d2ac18 (patch)
tree5dfe4cab0fd6ebe15c924bd83e3abb6efca210db
parentbebe8ec887ba67c51353e09d7758819b117bf62d (diff)
downloadmastodon-a38a452481d0f5207bb27ba7a2707c0028d2ac18.tar
mastodon-a38a452481d0f5207bb27ba7a2707c0028d2ac18.tar.gz
mastodon-a38a452481d0f5207bb27ba7a2707c0028d2ac18.tar.bz2
mastodon-a38a452481d0f5207bb27ba7a2707c0028d2ac18.zip
Add unread indicator to conversations (#9009)
-rw-r--r--app/controllers/api/v1/conversations_controller.rb20
-rw-r--r--app/controllers/api/v1/reports_controller.rb1
-rw-r--r--app/javascript/mastodon/actions/conversations.js11
-rw-r--r--app/javascript/mastodon/features/direct_timeline/components/conversation.js14
-rw-r--r--app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js8
-rw-r--r--app/javascript/mastodon/reducers/conversations.js10
-rw-r--r--app/javascript/styles/mastodon/components.scss5
-rw-r--r--app/models/account_conversation.rb2
-rw-r--r--app/serializers/rest/conversation_serializer.rb3
-rw-r--r--config/initializers/doorkeeper.rb2
-rw-r--r--config/routes.rb7
-rw-r--r--db/migrate/20181018205649_add_unread_to_account_conversations.rb23
-rw-r--r--db/schema.rb3
13 files changed, 98 insertions, 11 deletions
diff --git a/app/controllers/api/v1/conversations_controller.rb b/app/controllers/api/v1/conversations_controller.rb
index 736cb21ca..b19f27ebf 100644
--- a/app/controllers/api/v1/conversations_controller.rb
+++ b/app/controllers/api/v1/conversations_controller.rb
@@ -3,9 +3,11 @@
class Api::V1::ConversationsController < Api::BaseController
LIMIT = 20
- before_action -> { doorkeeper_authorize! :read, :'read:statuses' }
+ before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index
+ before_action -> { doorkeeper_authorize! :write, :'write:conversations' }, except: :index
before_action :require_user!
- after_action :insert_pagination_headers
+ before_action :set_conversation, except: :index
+ after_action :insert_pagination_headers, only: :index
respond_to :json
@@ -14,8 +16,22 @@ class Api::V1::ConversationsController < Api::BaseController
render json: @conversations, each_serializer: REST::ConversationSerializer
end
+ def read
+ @conversation.update!(unread: false)
+ render json: @conversation, serializer: REST::ConversationSerializer
+ end
+
+ def destroy
+ @conversation.destroy!
+ render_empty
+ end
+
private
+ def set_conversation
+ @conversation = AccountConversation.where(account: current_account).find(params[:id])
+ end
+
def paginated_conversations
AccountConversation.where(account: current_account)
.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index 726817927..e182a9c6c 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class Api::V1::ReportsController < Api::BaseController
- before_action -> { doorkeeper_authorize! :read, :'read:reports' }, except: [:create]
before_action -> { doorkeeper_authorize! :write, :'write:reports' }, only: [:create]
before_action :require_user!
diff --git a/app/javascript/mastodon/actions/conversations.js b/app/javascript/mastodon/actions/conversations.js
index cab05c1ba..aefd2fef7 100644
--- a/app/javascript/mastodon/actions/conversations.js
+++ b/app/javascript/mastodon/actions/conversations.js
@@ -13,6 +13,8 @@ export const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS';
export const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL';
export const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE';
+export const CONVERSATIONS_READ = 'CONVERSATIONS_READ';
+
export const mountConversations = () => ({
type: CONVERSATIONS_MOUNT,
});
@@ -21,6 +23,15 @@ export const unmountConversations = () => ({
type: CONVERSATIONS_UNMOUNT,
});
+export const markConversationRead = conversationId => (dispatch, getState) => {
+ dispatch({
+ type: CONVERSATIONS_READ,
+ id: conversationId,
+ });
+
+ api(getState).post(`/api/v1/conversations/${conversationId}/read`);
+};
+
export const expandConversations = ({ maxId } = {}) => (dispatch, getState) => {
dispatch(expandConversationsRequest());
diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
index f9a8d4f72..52e33c3c8 100644
--- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js
+++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js
@@ -8,6 +8,7 @@ import DisplayName from '../../../components/display_name';
import Avatar from '../../../components/avatar';
import AttachmentList from '../../../components/attachment_list';
import { HotKeys } from 'react-hotkeys';
+import classNames from 'classnames';
export default class Conversation extends ImmutablePureComponent {
@@ -19,8 +20,10 @@ export default class Conversation extends ImmutablePureComponent {
conversationId: PropTypes.string.isRequired,
accounts: ImmutablePropTypes.list.isRequired,
lastStatus: ImmutablePropTypes.map.isRequired,
+ unread:PropTypes.bool.isRequired,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
+ markRead: PropTypes.func.isRequired,
};
handleClick = () => {
@@ -28,7 +31,12 @@ export default class Conversation extends ImmutablePureComponent {
return;
}
- const { lastStatus } = this.props;
+ const { lastStatus, unread, markRead } = this.props;
+
+ if (unread) {
+ markRead();
+ }
+
this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
}
@@ -41,7 +49,7 @@ export default class Conversation extends ImmutablePureComponent {
}
render () {
- const { accounts, lastStatus, lastAccount } = this.props;
+ const { accounts, lastStatus, lastAccount, unread } = this.props;
if (lastStatus === null) {
return null;
@@ -61,7 +69,7 @@ export default class Conversation extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
- <div className='conversation focusable' tabIndex='0' onClick={this.handleClick} role='button'>
+ <div className={classNames('conversation', 'focusable', { 'conversation--unread': unread })} tabIndex='0' onClick={this.handleClick} role='button'>
<div className='conversation__header'>
<div className='conversation__avatars'>
<div>{accounts.map(account => <Avatar key={account.get('id')} size={36} account={account} />)}</div>
diff --git a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js
index 4166ee2ac..e2e2e3afb 100644
--- a/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js
+++ b/app/javascript/mastodon/features/direct_timeline/containers/conversation_container.js
@@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import Conversation from '../components/conversation';
+import { markConversationRead } from '../../../actions/conversations';
const mapStateToProps = (state, { conversationId }) => {
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
@@ -7,9 +8,14 @@ const mapStateToProps = (state, { conversationId }) => {
return {
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
+ unread: conversation.get('unread'),
lastStatus,
lastAccount: lastStatus === null ? null : state.getIn(['accounts', lastStatus.get('account')], null),
};
};
-export default connect(mapStateToProps)(Conversation);
+const mapDispatchToProps = (dispatch, { conversationId }) => ({
+ markRead: () => dispatch(markConversationRead(conversationId)),
+});
+
+export default connect(mapStateToProps, mapDispatchToProps)(Conversation);
diff --git a/app/javascript/mastodon/reducers/conversations.js b/app/javascript/mastodon/reducers/conversations.js
index 6b3f22d25..ea39fccee 100644
--- a/app/javascript/mastodon/reducers/conversations.js
+++ b/app/javascript/mastodon/reducers/conversations.js
@@ -6,6 +6,7 @@ import {
CONVERSATIONS_FETCH_SUCCESS,
CONVERSATIONS_FETCH_FAIL,
CONVERSATIONS_UPDATE,
+ CONVERSATIONS_READ,
} from '../actions/conversations';
import compareId from '../compare_id';
@@ -18,6 +19,7 @@ const initialState = ImmutableMap({
const conversationToMap = item => ImmutableMap({
id: item.id,
+ unread: item.unread,
accounts: ImmutableList(item.accounts.map(a => a.id)),
last_status: item.last_status.id,
});
@@ -80,6 +82,14 @@ export default function conversations(state = initialState, action) {
return state.update('mounted', count => count + 1);
case CONVERSATIONS_UNMOUNT:
return state.update('mounted', count => count - 1);
+ case CONVERSATIONS_READ:
+ return state.update('items', list => list.map(item => {
+ if (item.get('id') === action.id) {
+ return item.set('unread', false);
+ }
+
+ return item;
+ }));
default:
return state;
}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 129bde856..24b614a37 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -5503,6 +5503,11 @@ noscript {
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: pointer;
+ &--unread {
+ background: lighten($ui-base-color, 8%);
+ border-bottom-color: lighten($ui-base-color, 12%);
+ }
+
&__header {
display: flex;
margin-bottom: 15px;
diff --git a/app/models/account_conversation.rb b/app/models/account_conversation.rb
index c12c8d233..b7447d805 100644
--- a/app/models/account_conversation.rb
+++ b/app/models/account_conversation.rb
@@ -10,6 +10,7 @@
# status_ids :bigint(8) default([]), not null, is an Array
# last_status_id :bigint(8)
# lock_version :integer default(0), not null
+# unread :boolean default(FALSE), not null
#
class AccountConversation < ApplicationRecord
@@ -58,6 +59,7 @@ class AccountConversation < ApplicationRecord
def add_status(recipient, status)
conversation = find_or_initialize_by(account: recipient, conversation_id: status.conversation_id, participant_account_ids: participants_from_status(recipient, status))
conversation.status_ids << status.id
+ conversation.unread = status.account_id != recipient.id
conversation.save
conversation
rescue ActiveRecord::StaleObjectError
diff --git a/app/serializers/rest/conversation_serializer.rb b/app/serializers/rest/conversation_serializer.rb
index 884253f89..b09ca6341 100644
--- a/app/serializers/rest/conversation_serializer.rb
+++ b/app/serializers/rest/conversation_serializer.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
class REST::ConversationSerializer < ActiveModel::Serializer
- attribute :id
+ attributes :id, :unread
+
has_many :participant_accounts, key: :accounts, serializer: REST::AccountSerializer
has_one :last_status, serializer: REST::StatusSerializer
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index fe2490b32..367eead6a 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -58,6 +58,7 @@ Doorkeeper.configure do
optional_scopes :write,
:'write:accounts',
:'write:blocks',
+ :'write:conversations',
:'write:favourites',
:'write:filters',
:'write:follows',
@@ -76,7 +77,6 @@ Doorkeeper.configure do
:'read:lists',
:'read:mutes',
:'read:notifications',
- :'read:reports',
:'read:search',
:'read:statuses',
:follow,
diff --git a/config/routes.rb b/config/routes.rb
index a2468c9bd..b203e1329 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -261,7 +261,12 @@ Rails.application.routes.draw do
resources :streaming, only: [:index]
resources :custom_emojis, only: [:index]
resources :suggestions, only: [:index, :destroy]
- resources :conversations, only: [:index]
+
+ resources :conversations, only: [:index, :destroy] do
+ member do
+ post :read
+ end
+ end
get '/search', to: 'search#index', as: :search
diff --git a/db/migrate/20181018205649_add_unread_to_account_conversations.rb b/db/migrate/20181018205649_add_unread_to_account_conversations.rb
new file mode 100644
index 000000000..3c28b9a64
--- /dev/null
+++ b/db/migrate/20181018205649_add_unread_to_account_conversations.rb
@@ -0,0 +1,23 @@
+require Rails.root.join('lib', 'mastodon', 'migration_helpers')
+
+class AddUnreadToAccountConversations < ActiveRecord::Migration[5.2]
+ include Mastodon::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ safety_assured do
+ add_column_with_default(
+ :account_conversations,
+ :unread,
+ :boolean,
+ allow_null: false,
+ default: false
+ )
+ end
+ end
+
+ def down
+ remove_column :account_conversations, :unread, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f79f26f16..046975ac9 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2018_10_10_141500) do
+ActiveRecord::Schema.define(version: 2018_10_18_205649) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -22,6 +22,7 @@ ActiveRecord::Schema.define(version: 2018_10_10_141500) do
t.bigint "status_ids", default: [], null: false, array: true
t.bigint "last_status_id"
t.integer "lock_version", default: 0, null: false
+ t.boolean "unread", default: false, null: false
t.index ["account_id", "conversation_id", "participant_account_ids"], name: "index_unique_conversations", unique: true
t.index ["account_id"], name: "index_account_conversations_on_account_id"
t.index ["conversation_id"], name: "index_account_conversations_on_conversation_id"