Fix notifications
https://gitlab.com/soapbox-pub/soapbox/merge_requests/120/diffs#diff-content-917c7fb80cf426cfae1e7baaa21b3d90ccb2b29c
This commit is contained in:
parent
292991bf33
commit
7fb0b462e2
|
@ -1,8 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::V1::NotificationsController < Api::BaseController
|
class Api::V1::NotificationsController < Api::BaseController
|
||||||
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss]
|
before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss, :mark_read]
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss]
|
before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss, :mark_read]
|
||||||
before_action :require_user!
|
before_action :require_user!
|
||||||
after_action :insert_pagination_headers, only: :index
|
after_action :insert_pagination_headers, only: :index
|
||||||
|
|
||||||
|
@ -30,6 +30,11 @@ class Api::V1::NotificationsController < Api::BaseController
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mark_read
|
||||||
|
current_account.notifications.find(params[:id]).mark_read!
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_notifications
|
def load_notifications
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { unescapeHTML } from '../utils/html';
|
||||||
import { getFilters, regexFromFilters } from '../selectors';
|
import { getFilters, regexFromFilters } from '../selectors';
|
||||||
import { me } from 'gabsocial/initial_state';
|
import { me } from 'gabsocial/initial_state';
|
||||||
|
|
||||||
|
export const NOTIFICATIONS_INITIALIZE = 'NOTIFICATIONS_INITIALIZE';
|
||||||
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
|
||||||
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
|
||||||
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
|
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
|
||||||
|
@ -27,6 +28,7 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
|
||||||
|
|
||||||
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
|
||||||
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
|
||||||
|
export const NOTIFICATIONS_MARK_READ = 'NOTIFICATIONS_MARK_READ';
|
||||||
|
|
||||||
export const MAX_QUEUED_NOTIFICATIONS = 40;
|
export const MAX_QUEUED_NOTIFICATIONS = 40;
|
||||||
|
|
||||||
|
@ -43,6 +45,12 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function initializeNotifications() {
|
||||||
|
return {
|
||||||
|
type: NOTIFICATIONS_INITIALIZE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function updateNotifications(notification, intlMessages, intlLocale) {
|
export function updateNotifications(notification, intlMessages, intlLocale) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
|
||||||
|
@ -134,6 +142,7 @@ export function dequeueNotifications() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: NOTIFICATIONS_DEQUEUE,
|
type: NOTIFICATIONS_DEQUEUE,
|
||||||
});
|
});
|
||||||
|
dispatch(markReadNotifications());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -225,10 +234,13 @@ export function clearNotifications() {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function scrollTopNotifications(top) {
|
export function scrollTopNotifications(top) {
|
||||||
return {
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
type: NOTIFICATIONS_SCROLL_TOP,
|
type: NOTIFICATIONS_SCROLL_TOP,
|
||||||
top,
|
top,
|
||||||
};
|
});
|
||||||
|
dispatch(markReadNotifications());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setFilter (filterType) {
|
export function setFilter (filterType) {
|
||||||
|
@ -242,3 +254,20 @@ export function setFilter (filterType) {
|
||||||
dispatch(saveSettings());
|
dispatch(saveSettings());
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function markReadNotifications() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
if (!me) return;
|
||||||
|
const top_notification = parseInt(getState().getIn(['notifications', 'items', 0, 'id']));
|
||||||
|
const last_read = getState().getIn(['notifications', 'lastRead']);
|
||||||
|
|
||||||
|
if (top_notification && top_notification > last_read) {
|
||||||
|
api(getState).post('/api/v1/notifications/mark_read', {id: top_notification}).then(response => {
|
||||||
|
dispatch({
|
||||||
|
type: NOTIFICATIONS_MARK_READ,
|
||||||
|
notification: top_notification,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -14,7 +14,10 @@ import { isMobile } from '../../is_mobile';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { uploadCompose, resetCompose } from '../../actions/compose';
|
import { uploadCompose, resetCompose } from '../../actions/compose';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import { expandNotifications } from '../../actions/notifications';
|
import {
|
||||||
|
initializeNotifications,
|
||||||
|
expandNotifications,
|
||||||
|
} from '../../actions/notifications';
|
||||||
import { fetchFilters } from '../../actions/filters';
|
import { fetchFilters } from '../../actions/filters';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { openModal } from '../../actions/modal';
|
import { openModal } from '../../actions/modal';
|
||||||
|
@ -381,6 +384,7 @@ class UI extends React.PureComponent {
|
||||||
if (me) {
|
if (me) {
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
|
this.props.dispatch(initializeNotifications());
|
||||||
this.props.dispatch(fetchGroups('member'));
|
this.props.dispatch(fetchGroups('member'));
|
||||||
|
|
||||||
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
setTimeout(() => this.props.dispatch(fetchFilters()), 500);
|
||||||
|
|
|
@ -24,5 +24,6 @@ export const profile_directory = getMeta('profile_directory');
|
||||||
export const isStaff = getMeta('is_staff');
|
export const isStaff = getMeta('is_staff');
|
||||||
export const forceSingleColumn = !getMeta('advanced_layout');
|
export const forceSingleColumn = !getMeta('advanced_layout');
|
||||||
export const promotions = initialState && initialState.promotions;
|
export const promotions = initialState && initialState.promotions;
|
||||||
|
export const unreadCount = getMeta('unread_count');
|
||||||
|
|
||||||
export default initialState;
|
export default initialState;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
NOTIFICATIONS_INITIALIZE,
|
||||||
NOTIFICATIONS_UPDATE,
|
NOTIFICATIONS_UPDATE,
|
||||||
NOTIFICATIONS_EXPAND_SUCCESS,
|
NOTIFICATIONS_EXPAND_SUCCESS,
|
||||||
NOTIFICATIONS_EXPAND_REQUEST,
|
NOTIFICATIONS_EXPAND_REQUEST,
|
||||||
|
@ -9,6 +10,7 @@ import {
|
||||||
NOTIFICATIONS_UPDATE_QUEUE,
|
NOTIFICATIONS_UPDATE_QUEUE,
|
||||||
NOTIFICATIONS_DEQUEUE,
|
NOTIFICATIONS_DEQUEUE,
|
||||||
MAX_QUEUED_NOTIFICATIONS,
|
MAX_QUEUED_NOTIFICATIONS,
|
||||||
|
NOTIFICATIONS_MARK_READ,
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import {
|
import {
|
||||||
ACCOUNT_BLOCK_SUCCESS,
|
ACCOUNT_BLOCK_SUCCESS,
|
||||||
|
@ -17,6 +19,7 @@ import {
|
||||||
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
import { TIMELINE_DELETE, TIMELINE_DISCONNECT } from '../actions/timelines';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
import compareId from '../compare_id';
|
import compareId from '../compare_id';
|
||||||
|
import { unreadCount } from 'gabsocial/initial_state';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
items: ImmutableList(),
|
items: ImmutableList(),
|
||||||
|
@ -26,6 +29,7 @@ const initialState = ImmutableMap({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
|
queuedNotifications: ImmutableList(), //max = MAX_QUEUED_NOTIFICATIONS
|
||||||
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
|
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+
|
||||||
|
lastRead: -1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const notificationToMap = notification => ImmutableMap({
|
const notificationToMap = notification => ImmutableMap({
|
||||||
|
@ -126,6 +130,10 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
|
||||||
|
|
||||||
export default function notifications(state = initialState, action) {
|
export default function notifications(state = initialState, action) {
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
|
case NOTIFICATIONS_INITIALIZE:
|
||||||
|
return state.set('unread', unreadCount);
|
||||||
|
case NOTIFICATIONS_MARK_READ:
|
||||||
|
return state.set('lastRead', action.notification);
|
||||||
case NOTIFICATIONS_EXPAND_REQUEST:
|
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||||
return state.set('isLoading', true);
|
return state.set('isLoading', true);
|
||||||
case NOTIFICATIONS_EXPAND_FAIL:
|
case NOTIFICATIONS_EXPAND_FAIL:
|
||||||
|
|
|
@ -74,6 +74,17 @@ class Notification < ApplicationRecord
|
||||||
type != :follow_request
|
type != :follow_request
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mark_read!
|
||||||
|
user = account.user
|
||||||
|
is_newer = self.id > (user.last_read_notification || -1)
|
||||||
|
|
||||||
|
if is_newer
|
||||||
|
user.last_read_notification = self.id
|
||||||
|
user.save!
|
||||||
|
else false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def cache_ids
|
def cache_ids
|
||||||
select(:id, :updated_at, :activity_type, :activity_id)
|
select(:id, :updated_at, :activity_type, :activity_id)
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
# chosen_languages :string is an Array
|
# chosen_languages :string is an Array
|
||||||
# created_by_application_id :bigint(8)
|
# created_by_application_id :bigint(8)
|
||||||
# approved :boolean default(TRUE), not null
|
# approved :boolean default(TRUE), not null
|
||||||
|
# last_read_notification :bigint(8)
|
||||||
#
|
#
|
||||||
|
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
|
|
@ -36,6 +36,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
|
||||||
store[:group_in_home_feed] = object.current_account.user.setting_group_in_home_feed
|
store[:group_in_home_feed] = object.current_account.user.setting_group_in_home_feed
|
||||||
store[:is_staff] = object.current_account.user.staff?
|
store[:is_staff] = object.current_account.user.staff?
|
||||||
|
store[:unread_count] = unread_count object.current_account
|
||||||
end
|
end
|
||||||
|
|
||||||
store
|
store
|
||||||
|
@ -78,6 +79,11 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def unread_count(account)
|
||||||
|
last_read = account.user.last_read_notification || 0
|
||||||
|
account.notifications.where("id > #{last_read}").count
|
||||||
|
end
|
||||||
|
|
||||||
def instance_presenter
|
def instance_presenter
|
||||||
@instance_presenter ||= InstancePresenter.new
|
@instance_presenter ||= InstancePresenter.new
|
||||||
end
|
end
|
||||||
|
|
|
@ -369,6 +369,7 @@ Rails.application.routes.draw do
|
||||||
collection do
|
collection do
|
||||||
post :clear
|
post :clear
|
||||||
post :dismiss # Deprecated
|
post :dismiss # Deprecated
|
||||||
|
post :mark_read
|
||||||
end
|
end
|
||||||
|
|
||||||
member do
|
member do
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddLastReadNotificationToUser < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_column :users, :last_read_notification, :bigint
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddForeignKeyConstraintToUserLastReadNotification < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
add_foreign_key :users, :notifications, column: :last_read_notification, on_delete: :nullify, validate: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
class ValidateAddForeignKeyConstraintToUserLastReadNotification < ActiveRecord::Migration[5.2]
|
||||||
|
def change
|
||||||
|
validate_foreign_key :users, :notifications
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2019_09_17_141707) do
|
ActiveRecord::Schema.define(version: 2019_12_01_210057) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
@ -823,6 +823,7 @@ ActiveRecord::Schema.define(version: 2019_09_17_141707) do
|
||||||
t.string "chosen_languages", array: true
|
t.string "chosen_languages", array: true
|
||||||
t.bigint "created_by_application_id"
|
t.bigint "created_by_application_id"
|
||||||
t.boolean "approved", default: true, null: false
|
t.boolean "approved", default: true, null: false
|
||||||
|
t.bigint "last_read_notification"
|
||||||
t.index ["account_id"], name: "index_users_on_account_id"
|
t.index ["account_id"], name: "index_users_on_account_id"
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
||||||
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
|
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id"
|
||||||
|
@ -934,6 +935,7 @@ ActiveRecord::Schema.define(version: 2019_09_17_141707) do
|
||||||
add_foreign_key "user_invite_requests", "users", on_delete: :cascade
|
add_foreign_key "user_invite_requests", "users", on_delete: :cascade
|
||||||
add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade
|
add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade
|
||||||
add_foreign_key "users", "invites", on_delete: :nullify
|
add_foreign_key "users", "invites", on_delete: :nullify
|
||||||
|
add_foreign_key "users", "notifications", column: "last_read_notification", on_delete: :nullify
|
||||||
add_foreign_key "users", "oauth_applications", column: "created_by_application_id", on_delete: :nullify
|
add_foreign_key "users", "oauth_applications", column: "created_by_application_id", on_delete: :nullify
|
||||||
add_foreign_key "web_push_subscriptions", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade
|
add_foreign_key "web_push_subscriptions", "oauth_access_tokens", column: "access_token_id", on_delete: :cascade
|
||||||
add_foreign_key "web_push_subscriptions", "users", on_delete: :cascade
|
add_foreign_key "web_push_subscriptions", "users", on_delete: :cascade
|
||||||
|
|
Loading…
Reference in New Issue