Merge branch 'main' into feature/raised-texts
This commit is contained in:
commit
6b3342036f
|
@ -437,7 +437,7 @@ GEM
|
|||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
rubyzip (2.3.2)
|
||||
sanitize (6.0.0)
|
||||
sanitize (6.0.1)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
sassc (2.4.0)
|
||||
|
|
9
Rakefile
9
Rakefile
|
@ -51,6 +51,15 @@ namespace :justask do # rubocop:disable Metrics/BlockLength
|
|||
user.remove_role :moderator
|
||||
puts "#{user.screen_name} is no longer a moderator."
|
||||
end
|
||||
|
||||
desc "Removes users whose accounts haven't been verified for over 3 months."
|
||||
task remove_stale: :environment do
|
||||
puts "Removing stale users…"
|
||||
removed = User.where(confirmed_at: nil)
|
||||
.where("confirmation_sent_at < ?", DateTime.now.utc - 3.months)
|
||||
.destroy_all.count
|
||||
puts "Removed #{removed} users"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :db do
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AjaxController < ApplicationController
|
||||
skip_before_action :find_active_announcements
|
||||
before_action :build_response
|
||||
after_action :return_response
|
||||
|
||||
|
@ -92,10 +93,6 @@ class AjaxController < ApplicationController
|
|||
return_response
|
||||
end
|
||||
|
||||
def find_active_announcements
|
||||
# We do not need announcements here
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_response
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
class InboxController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
after_action :mark_inbox_entries_as_read, only: %i[show]
|
||||
|
||||
def show
|
||||
find_author
|
||||
find_inbox_entries
|
||||
|
@ -77,4 +79,9 @@ class InboxController < ApplicationController
|
|||
.joins(:question)
|
||||
.where(questions: { user: @author_user, author_is_anonymous: false })
|
||||
end
|
||||
|
||||
def mark_inbox_entries_as_read
|
||||
# using .dup to not modify @inbox -- useful in tests
|
||||
@inbox&.dup&.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,13 @@
|
|||
class ManifestsController < ApplicationController
|
||||
include ThemeHelper
|
||||
|
||||
skip_before_action :banned?
|
||||
skip_before_action :find_active_announcements
|
||||
|
||||
def show
|
||||
expires_in 1.day
|
||||
return if fresh_when current_user&.theme
|
||||
|
||||
render json: {
|
||||
name: APP_CONFIG["site_name"],
|
||||
description: t("about.index.subtitle"),
|
||||
|
@ -12,17 +18,19 @@ class ManifestsController < ApplicationController
|
|||
display: "standalone",
|
||||
categories: %w[social],
|
||||
lang: I18n.locale,
|
||||
shortcuts: [
|
||||
webapp_shortcut(inbox_url, t("navigation.inbox"), "inbox")
|
||||
],
|
||||
shortcuts:,
|
||||
icons: webapp_icons,
|
||||
theme_color: theme_color,
|
||||
theme_color:,
|
||||
background_color: mobile_theme_color
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def shortcuts = [
|
||||
webapp_shortcut(inbox_url, t("navigation.inbox"), "inbox")
|
||||
]
|
||||
|
||||
def webapp_shortcut(url, name, icon_name)
|
||||
{
|
||||
name: name,
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
class NotificationsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
after_action :mark_notifications_as_read, only: %i[index]
|
||||
|
||||
TYPE_MAPPINGS = {
|
||||
"answer" => Notification::QuestionAnswered.name,
|
||||
"comment" => Notification::Commented.name,
|
||||
|
@ -25,6 +27,11 @@ class NotificationsController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def mark_notifications_as_read
|
||||
# using .dup to not modify @notifications -- useful in tests
|
||||
@notifications&.dup&.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def cursored_notifications_for(type:, last_id:, size: nil)
|
||||
cursor_params = { last_id: last_id, size: size }.compact
|
||||
|
||||
|
|
|
@ -3,20 +3,13 @@
|
|||
class UserController < ApplicationController
|
||||
before_action :set_user
|
||||
before_action :hidden_social_graph_redirect, only: %i[followers followings]
|
||||
after_action :mark_notification_as_read, only: %i[show]
|
||||
|
||||
def show
|
||||
@answers = @user.cursored_answers(last_id: params[:last_id])
|
||||
@answers_last_id = @answers.map(&:id).min
|
||||
@more_data_available = !@user.cursored_answers(last_id: @answers_last_id, size: 1).count.zero?
|
||||
|
||||
if user_signed_in?
|
||||
notif = Notification.where(target_type: "Relationship", target_id: @user.active_follow_relationships.where(target_id: current_user.id).pluck(:id), recipient_id: current_user.id, new: true).first
|
||||
unless notif.nil?
|
||||
notif.new = false
|
||||
notif.save
|
||||
end
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.turbo_stream
|
||||
|
@ -24,36 +17,30 @@ class UserController < ApplicationController
|
|||
end
|
||||
|
||||
def followers
|
||||
@title = "Followers"
|
||||
@relationships = @user.cursored_follower_relationships(last_id: params[:last_id])
|
||||
@relationships_last_id = @relationships.map(&:id).min
|
||||
@more_data_available = !@user.cursored_follower_relationships(last_id: @relationships_last_id, size: 1).count.zero?
|
||||
@users = @relationships.map(&:source)
|
||||
@type = :follower
|
||||
|
||||
respond_to do |format|
|
||||
format.html { render "show_follow" }
|
||||
format.turbo_stream { render "show_follow" }
|
||||
format.html { render "show_follow", locals: { type: :follower } }
|
||||
format.turbo_stream { render "show_follow", locals: { type: :follower } }
|
||||
end
|
||||
end
|
||||
|
||||
def followings
|
||||
@title = "Following"
|
||||
@relationships = @user.cursored_following_relationships(last_id: params[:last_id])
|
||||
@relationships_last_id = @relationships.map(&:id).min
|
||||
@more_data_available = !@user.cursored_following_relationships(last_id: @relationships_last_id, size: 1).count.zero?
|
||||
@users = @relationships.map(&:target)
|
||||
@type = :friend
|
||||
|
||||
respond_to do |format|
|
||||
format.html { render "show_follow" }
|
||||
format.turbo_stream { render "show_follow" }
|
||||
format.html { render "show_follow", locals: { type: :friend } }
|
||||
format.turbo_stream { render "show_follow", locals: { type: :friend } }
|
||||
end
|
||||
end
|
||||
|
||||
def questions
|
||||
@title = "Questions"
|
||||
|
||||
@questions = @user.cursored_questions(author_is_anonymous: false, direct: direct_param, last_id: params[:last_id])
|
||||
@questions_last_id = @questions.map(&:id).min
|
||||
@more_data_available = !@user.cursored_questions(author_is_anonymous: false, direct: direct_param, last_id: @questions_last_id, size: 1).count.zero?
|
||||
|
@ -66,6 +53,18 @@ class UserController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def mark_notification_as_read
|
||||
return unless user_signed_in?
|
||||
|
||||
Notification
|
||||
.where(
|
||||
target_type: "Relationship",
|
||||
target_id: @user.active_follow_relationships.where(target_id: current_user.id).pluck(:id),
|
||||
recipient_id: current_user.id,
|
||||
new: true
|
||||
).update(new: false)
|
||||
end
|
||||
|
||||
def set_user
|
||||
@user = User.where("LOWER(screen_name) = ?", params[:username].downcase).includes(:profile).first!
|
||||
end
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import Croppr from 'croppr';
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ['input', 'controls', 'cropper', 'x', 'y', 'w', 'h'];
|
||||
|
||||
declare readonly inputTarget: HTMLInputElement;
|
||||
declare readonly controlsTarget: HTMLElement;
|
||||
declare readonly cropperTarget: HTMLImageElement;
|
||||
declare readonly xTarget: HTMLInputElement;
|
||||
declare readonly yTarget: HTMLInputElement;
|
||||
declare readonly wTarget: HTMLInputElement;
|
||||
declare readonly hTarget: HTMLInputElement;
|
||||
|
||||
static values = {
|
||||
aspectRatio: String
|
||||
};
|
||||
|
||||
declare readonly aspectRatioValue: string;
|
||||
|
||||
readImage(file: File, callback: (string) => void): void {
|
||||
callback((window.URL || window.webkitURL).createObjectURL(file));
|
||||
}
|
||||
|
||||
updateValues(data: Record<string, string>): void {
|
||||
this.xTarget.value = data.x;
|
||||
this.yTarget.value = data.y;
|
||||
this.wTarget.value = data.width;
|
||||
this.hTarget.value = data.height;
|
||||
}
|
||||
|
||||
change(): void {
|
||||
this.controlsTarget.classList.toggle('d-none');
|
||||
|
||||
if (this.inputTarget.files && this.inputTarget.files[0]) {
|
||||
this.readImage(this.inputTarget.files[0], (src) => {
|
||||
this.cropperTarget.src = src;
|
||||
|
||||
new Croppr(this.cropperTarget, {
|
||||
aspectRatio: parseFloat(this.aspectRatioValue),
|
||||
startSize: [100, 100, '%'],
|
||||
onCropStart: this.updateValues.bind(this),
|
||||
onCropMove: this.updateValues.bind(this),
|
||||
onCropEnd: this.updateValues.bind(this)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export function authorSearchHandler(event: Event): void {
|
||||
event.preventDefault();
|
||||
|
||||
const author = document.querySelector<HTMLInputElement>('#author')?.value;
|
||||
window.location.href = `/inbox/${encodeURIComponent(author)}`;
|
||||
}
|
|
@ -62,6 +62,7 @@ export function deleteAllQuestionsHandler(event: Event): void {
|
|||
export function deleteAllAuthorQuestionsHandler(event: Event): void {
|
||||
const button = event.target as Element;
|
||||
const count = button.getAttribute('data-ib-count');
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
swal({
|
||||
title: I18n.translate('frontend.inbox.confirm_all.title', { count: count }),
|
||||
|
@ -75,7 +76,7 @@ export function deleteAllAuthorQuestionsHandler(event: Event): void {
|
|||
}, (returnValue) => {
|
||||
if (returnValue === null) return false;
|
||||
|
||||
post(`/ajax/delete_all_inbox/${location.pathname.split('/')[2]}`)
|
||||
post(`/ajax/delete_all_inbox/${urlSearchParams.get('author')}`)
|
||||
.then(async response => {
|
||||
const data = await response.json;
|
||||
|
||||
|
@ -89,4 +90,4 @@ export function deleteAllAuthorQuestionsHandler(event: Event): void {
|
|||
showErrorNotification(I18n.translate('frontend.error.message'));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,7 @@ import { deleteAllAuthorQuestionsHandler, deleteAllQuestionsHandler } from './de
|
|||
export default (): void => {
|
||||
registerEvents([
|
||||
{ type: 'click', target: '#ib-delete-all', handler: deleteAllQuestionsHandler, global: true },
|
||||
{ type: 'click', target: '#ib-delete-all-author', handler: deleteAllAuthorQuestionsHandler, global: true },
|
||||
{ type: 'submit', target: '#author-form', handler: authorSearchHandler, global: true }
|
||||
{ type: 'click', target: '#ib-delete-all-author', handler: deleteAllAuthorQuestionsHandler, global: true }
|
||||
]);
|
||||
|
||||
registerInboxEntryEvents();
|
||||
|
|
|
@ -1,61 +0,0 @@
|
|||
import Croppr from 'croppr';
|
||||
|
||||
const readImage = (file, callback) => callback((window.URL || window.webkitURL).createObjectURL(file));
|
||||
|
||||
export function profilePictureChangeHandler(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
|
||||
const cropControls = document.querySelector('#profile-picture-crop-controls');
|
||||
cropControls.classList.toggle('d-none');
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
readImage(input.files[0], (src) => {
|
||||
const updateValues = (data) => {
|
||||
document.querySelector<HTMLInputElement>('#profile_picture_x').value = data.x;
|
||||
document.querySelector<HTMLInputElement>('#profile_picture_y').value = data.y;
|
||||
document.querySelector<HTMLInputElement>('#profile_picture_w').value = data.width;
|
||||
document.querySelector<HTMLInputElement>('#profile_picture_h').value = data.height;
|
||||
}
|
||||
|
||||
const cropper = document.querySelector<HTMLImageElement>('#profile-picture-cropper');
|
||||
cropper.src = src;
|
||||
|
||||
new Croppr(cropper, {
|
||||
aspectRatio: 1,
|
||||
startSize: [100, 100, '%'],
|
||||
onCropStart: updateValues,
|
||||
onCropMove: updateValues,
|
||||
onCropEnd: updateValues
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function profileHeaderChangeHandler(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
|
||||
const cropControls = document.querySelector('#profile-header-crop-controls');
|
||||
cropControls.classList.toggle('d-none');
|
||||
|
||||
if (input.files && input.files[0]) {
|
||||
readImage(input.files[0], (src) => {
|
||||
const updateValues = (data) => {
|
||||
document.querySelector<HTMLInputElement>('#profile_header_x').value = data.x;
|
||||
document.querySelector<HTMLInputElement>('#profile_header_y').value = data.y;
|
||||
document.querySelector<HTMLInputElement>('#profile_header_w').value = data.width;
|
||||
document.querySelector<HTMLInputElement>('#profile_header_h').value = data.height;
|
||||
}
|
||||
|
||||
const cropper = document.querySelector<HTMLImageElement>('#profile-header-cropper');
|
||||
cropper.src = src;
|
||||
|
||||
new Croppr(cropper, {
|
||||
aspectRatio: 7/30,
|
||||
startSize: [100, 100, '%'],
|
||||
onCropStart: updateValues,
|
||||
onCropMove: updateValues,
|
||||
onCropEnd: updateValues
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,11 +1,8 @@
|
|||
import registerEvents from "utilities/registerEvents";
|
||||
import { profileHeaderChangeHandler, profilePictureChangeHandler } from "./crop";
|
||||
import { userSubmitHandler } from "./password";
|
||||
|
||||
export default (): void => {
|
||||
registerEvents([
|
||||
{ type: 'submit', target: document.querySelector('#edit_user'), handler: userSubmitHandler },
|
||||
{ type: 'change', target: document.querySelector('#user_profile_picture[type=file]'), handler: profilePictureChangeHandler },
|
||||
{ type: 'change', target: document.querySelector('#user_profile_header[type=file]'), handler: profileHeaderChangeHandler }
|
||||
{ type: 'submit', target: document.querySelector('#edit_user'), handler: userSubmitHandler }
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import FormatPopupController from "retrospring/controllers/format_popup_controll
|
|||
import CollapseController from "retrospring/controllers/collapse_controller";
|
||||
import ThemeController from "retrospring/controllers/theme_controller";
|
||||
import CapabilitiesController from "retrospring/controllers/capabilities_controller";
|
||||
import CropperController from "retrospring/controllers/cropper_controller";
|
||||
|
||||
/**
|
||||
* This module sets up Stimulus and our controllers
|
||||
|
@ -23,6 +24,7 @@ export default function (): void {
|
|||
window['Stimulus'].register('character-count', CharacterCountController);
|
||||
window['Stimulus'].register('character-count-warning', CharacterCountWarningController);
|
||||
window['Stimulus'].register('collapse', CollapseController);
|
||||
window['Stimulus'].register('cropper', CropperController);
|
||||
window['Stimulus'].register('format-popup', FormatPopupController);
|
||||
window['Stimulus'].register('theme', ThemeController);
|
||||
}
|
||||
|
|
|
@ -5,10 +5,11 @@ module User::AnswerMethods
|
|||
|
||||
define_cursor_paginator :cursored_answers, :ordered_answers
|
||||
|
||||
# @return [ActiveRecord::Relation<Answer>] List of a user's answers
|
||||
def ordered_answers
|
||||
answers
|
||||
.order(:created_at)
|
||||
.reverse_order
|
||||
.includes(comments: [:user, :smiles], question: [:user], smiles: [:user])
|
||||
.includes(comments: %i[user smiles], question: { user: :profile }, smiles: [:user])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,9 +5,10 @@ module User::InboxMethods
|
|||
|
||||
define_cursor_paginator :cursored_inbox, :ordered_inbox
|
||||
|
||||
# @return [ActiveRecord::Relation<Inbox>] the user's inbox entries
|
||||
def ordered_inbox
|
||||
inboxes
|
||||
.includes(:question, :user)
|
||||
.includes(:question, user: :profile)
|
||||
.order(:created_at)
|
||||
.reverse_order
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ module User::QuestionMethods
|
|||
|
||||
define_cursor_paginator :cursored_questions, :ordered_questions
|
||||
|
||||
# @return [ActiveRecord::Relation<Question>] List of questions sent by the user
|
||||
def ordered_questions(author_is_anonymous: nil, direct: nil)
|
||||
questions
|
||||
.where({ author_is_anonymous:, direct: }.compact)
|
||||
|
|
|
@ -6,10 +6,12 @@ module User::RelationshipMethods
|
|||
define_cursor_paginator :cursored_following_relationships, :ordered_following_relationships
|
||||
define_cursor_paginator :cursored_follower_relationships, :ordered_follower_relationships
|
||||
|
||||
# @return [ActiveRecord::Relation<Relationships::Follow>] List of the user's following relationships
|
||||
def ordered_following_relationships
|
||||
active_follow_relationships.reverse_order.includes(target: [:profile])
|
||||
end
|
||||
|
||||
# @return [ActiveRecord::Relation<Relationships::Follow>] List of the user's follower relationships
|
||||
def ordered_follower_relationships
|
||||
passive_follow_relationships.reverse_order.includes(source: [:profile])
|
||||
end
|
||||
|
|
|
@ -5,8 +5,12 @@ module User::TimelineMethods
|
|||
|
||||
define_cursor_paginator :cursored_timeline, :timeline
|
||||
|
||||
# @return [Array] the users' timeline
|
||||
# @return [ActiveRecord::Relation<Answer>] the user's timeline
|
||||
def timeline
|
||||
Answer.where("user_id in (?) OR user_id = ?", following_ids, id).order(:created_at).reverse_order.includes(comments: %i[user smiles], question: [:user], user: [:profile], smiles: [:user])
|
||||
Answer
|
||||
.where("user_id in (?) OR user_id = ?", following_ids, id)
|
||||
.order(:created_at)
|
||||
.reverse_order
|
||||
.includes(comments: %i[user smiles], question: { user: :profile }, user: [:profile], smiles: [:user])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,6 +19,7 @@ class TypoedEmailValidator < ActiveModel::EachValidator
|
|||
gmaile.com
|
||||
gmaill.com
|
||||
gmali.com
|
||||
gmaul.com
|
||||
gnail.com
|
||||
hotamil.com
|
||||
hotmai.com
|
||||
|
|
|
@ -9,6 +9,5 @@
|
|||
= render 'shared/links'
|
||||
|
||||
:ruby
|
||||
@inbox.update_all(new: false)
|
||||
provide(:title, generate_title('Inbox'))
|
||||
parent_layout 'base'
|
||||
|
|
|
@ -8,5 +8,4 @@
|
|||
.d-block.d-sm-none= render "shared/links"
|
||||
|
||||
:ruby
|
||||
Notification.for(current_user).update_all(new: false)
|
||||
parent_layout 'base'
|
||||
|
|
|
@ -2,36 +2,38 @@
|
|||
.card-body
|
||||
= bootstrap_form_for(current_user, url: settings_profile_picture_path, html: { multipart: true }, method: :patch, data: { turbo: false }) do |f|
|
||||
|
||||
.d-flex#profile-picture-media
|
||||
.flex-shrink-0
|
||||
%img.avatar-lg.me-3{ src: current_user.profile_picture.url(:medium) }
|
||||
.flex-grow-1
|
||||
= f.file_field :profile_picture, accept: APP_CONFIG[:accepted_image_formats].join(",")
|
||||
%div{ data: { controller: "cropper", cropper_aspect_ratio_value: "1" } }
|
||||
.d-flex
|
||||
.flex-shrink-0
|
||||
%img.avatar-lg.me-3{ src: current_user.profile_picture.url(:medium) }
|
||||
.flex-grow-1
|
||||
= f.file_field :profile_picture, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" }
|
||||
|
||||
.row.d-none#profile-picture-crop-controls
|
||||
.col-sm-10.col-md-8
|
||||
%strong= t(".adjust.profile_picture")
|
||||
%img#profile-picture-cropper{ src: current_user.profile_picture.url(:medium) }
|
||||
.row.d-none{ data: { cropper_target: "controls" } }
|
||||
.col-sm-10.col-md-8
|
||||
%strong= t(".adjust.profile_picture")
|
||||
%img{ src: current_user.profile_picture.url(:medium), data: { cropper_target: "cropper" } }
|
||||
|
||||
.row.mb-2#profile-header-media
|
||||
.col-xs-12.col-md-6
|
||||
%img.mw-100.me-3{ src: current_user.profile_header.url(:mobile) }
|
||||
.col-xs-12.col-md-6.mt-3.mt-sm-0.ps-3.pe-3
|
||||
= f.file_field :profile_header, accept: APP_CONFIG[:accepted_image_formats].join(",")
|
||||
- %i[profile_picture_x profile_picture_y profile_picture_w profile_picture_h].each do |attrib|
|
||||
= f.hidden_field attrib, id: attrib, data: { cropper_target: attrib.to_s.split("_").last }
|
||||
|
||||
.row.d-none#profile-header-crop-controls
|
||||
.col-sm-10.col-md-8
|
||||
%strong= t(".adjust.profile_header")
|
||||
%img#profile-header-cropper{ src: current_user.profile_header.url(:web) }
|
||||
%div{ data: { controller: "cropper", cropper_aspect_ratio_value: "0.23" } }
|
||||
.row.mb-2
|
||||
.col-xs-12.col-md-6
|
||||
%img.mw-100.me-3{ src: current_user.profile_header.url(:mobile) }
|
||||
.col-xs-12.col-md-6.mt-3.mt-sm-0.ps-3.pe-3
|
||||
= f.file_field :profile_header, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" }
|
||||
|
||||
.row.d-none{ data: { cropper_target: "controls" } }
|
||||
.col-sm-10.col-md-8
|
||||
%strong= t(".adjust.profile_header")
|
||||
%img{ src: current_user.profile_header.url(:web), data: { cropper_target: "cropper" } }
|
||||
|
||||
- %i[profile_header_x profile_header_y profile_header_w profile_header_h].each do |attrib|
|
||||
= f.hidden_field attrib, id: attrib, data: { cropper_target: attrib.to_s.split("_").last }
|
||||
|
||||
= f.check_box :show_foreign_themes
|
||||
|
||||
- %i[profile_picture_x profile_picture_y profile_picture_w profile_picture_h].each do |attrib|
|
||||
= f.hidden_field attrib, id: attrib
|
||||
|
||||
- %i[profile_header_x profile_header_y profile_header_w profile_header_h].each do |attrib|
|
||||
= f.hidden_field attrib, id: attrib
|
||||
|
||||
= f.primary t(".submit_picture")
|
||||
.card
|
||||
.card-body
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
.row.row-cols-1.row-cols-sm-2.row-cols-md-3#users
|
||||
- @users.each do |user|
|
||||
.col.pb-3
|
||||
= render 'shared/userbox', user: user, type: @type
|
||||
= render "shared/userbox", user:, type:
|
||||
|
||||
- if @more_data_available
|
||||
.d-flex.justify-content-center.justify-content-sm-start#paginator
|
||||
= button_to t("voc.load"), @type == :follower ? show_user_followers_path(@user) : show_user_followings_path(@user),
|
||||
= button_to t("voc.load"), type == :follower ? show_user_followers_path(@user) : show_user_followings_path(@user),
|
||||
class: "btn btn-light",
|
||||
method: :get,
|
||||
params: { last_id: @relationships_last_id },
|
||||
form: { data: { turbo_stream: true } }
|
||||
|
||||
- provide(:title, user_title(@user, 'friends and followers'))
|
||||
- parent_layout 'user/profile'
|
||||
- provide(:title, t(".title.#{type}", user: @user.profile.safe_name))
|
||||
- parent_layout "user/profile"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
= turbo_stream.append "users" do
|
||||
- @users.each do |user|
|
||||
.col.pb-3
|
||||
= render 'shared/userbox', user: user, type: @type
|
||||
= render "shared/userbox", user:, type:
|
||||
|
||||
= turbo_stream.update "paginator" do
|
||||
- if @more_data_available
|
||||
= button_to t("voc.load"), @type == :follower ? show_user_followers_path(@user) : show_user_followings_path(@user),
|
||||
= button_to t("voc.load"), type == :follower ? show_user_followers_path(@user) : show_user_followings_path(@user),
|
||||
class: "btn btn-light",
|
||||
method: :get,
|
||||
params: { last_id: @relationships_last_id },
|
||||
|
|
|
@ -8,4 +8,20 @@ Sentry.init do |config|
|
|||
# of transactions for performance monitoring.
|
||||
# We recommend adjusting this value in production
|
||||
config.traces_sample_rate = 0.25
|
||||
|
||||
exception_fingerprints = {
|
||||
Excon::Error::ServiceUnavailable => 'external-service',
|
||||
Twitter::Error::InternalServerError => 'external-service',
|
||||
}
|
||||
config.before_send = lambda do |event, hint|
|
||||
# These are used for user-facing errors, not when something goes wrong
|
||||
next if hint[:exception].is_a?(Errors::Base)
|
||||
|
||||
exception_class = hint[:exception].class
|
||||
if exception_fingerprints.key?(exception_class)
|
||||
event.fingerprint = [exception_fingerprints[exception_class]]
|
||||
end
|
||||
|
||||
event
|
||||
end
|
||||
end
|
||||
|
|
|
@ -651,6 +651,10 @@ en:
|
|||
index:
|
||||
title: "Questions from %{author_identifier}"
|
||||
user:
|
||||
show_follow:
|
||||
title:
|
||||
follower: "%{user}'s followers"
|
||||
friend: "%{user}'s followings"
|
||||
actions:
|
||||
view_inbox: "View inbox"
|
||||
privilege: "Check %{user}'s privileges"
|
||||
|
|
|
@ -157,14 +157,14 @@ Rails.application.routes.draw do
|
|||
get "/@:username/q/:id", to: "question#show", as: :question
|
||||
get "/@:username/followers", to: "user#followers", as: :show_user_followers
|
||||
get "/@:username/followings", to: "user#followings", as: :show_user_followings
|
||||
get "/@:username/friends", to: redirect("/@%{username}/followings/p/%{page}")
|
||||
get "/@:username/friends", to: redirect("/@%{username}/followings")
|
||||
get "/@:username/questions", to: "user#questions", as: :show_user_questions
|
||||
get "/:username", to: "user#show", as: :user_alt
|
||||
get "/:username/a/:id", to: "answer#show", as: :answer_alt
|
||||
get "/:username/q/:id", to: "question#show", as: :question_alt
|
||||
get "/:username/followers", to: "user#followers", as: :show_user_followers_alt
|
||||
get "/:username/followings", to: "user#followings", as: :show_user_followings_alt
|
||||
get "/:username/friends", to: redirect("/%{username}/followings/p/%{page}")
|
||||
get "/:username/friends", to: redirect("/%{username}/followings")
|
||||
get "/:username/questions", to: "user#questions", as: :show_user_questions_alt
|
||||
|
||||
get "/feedback/consent", to: "feedback#consent", as: "feedback_consent"
|
||||
|
|
|
@ -17,7 +17,7 @@ module Retrospring
|
|||
|
||||
def month = 1
|
||||
|
||||
def day = 23
|
||||
def day = 27
|
||||
|
||||
def patch = 0
|
||||
|
||||
|
|
|
@ -47,6 +47,9 @@ self.addEventListener('install', function (event) {
|
|||
});
|
||||
|
||||
self.addEventListener('fetch', function (event) {
|
||||
const url = new URL(event.request.url);
|
||||
if (event.request.method !== 'GET' || !OFFLINE_CACHE_PATHS.includes(url.pathname)) return;
|
||||
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
|
|
|
@ -58,6 +58,10 @@ describe InboxController, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
it "updates the inbox entry status" do
|
||||
expect { subject }.to change { inbox_entry.reload.new? }.from(true).to(false)
|
||||
end
|
||||
|
||||
context "when requested the turbo stream format" do
|
||||
subject { get :show, format: :turbo_stream }
|
||||
|
||||
|
|
|
@ -33,5 +33,9 @@ describe NotificationsController do
|
|||
expect(response).to render_template(:index)
|
||||
expect(controller.instance_variable_get(:@notifications)).to have_attributes(size: 2)
|
||||
end
|
||||
|
||||
it "marks notifications as read" do
|
||||
expect { subject }.to change { Notification.for(user).where(new: true).count }.from(2).to(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -93,6 +93,7 @@ RSpec.describe User, type: :model do
|
|||
include_examples "invalid email", "fritz.fantom@gmaile.com"
|
||||
include_examples "invalid email", "fritz.fantom@gmaill.com"
|
||||
include_examples "invalid email", "fritz.fantom@gmali.com"
|
||||
include_examples "invalid email", "fritz.fantom@gmaul.com"
|
||||
include_examples "invalid email", "fritz.fantom@gnail.com"
|
||||
include_examples "invalid email", "fritz.fantom@hotamil.com"
|
||||
include_examples "invalid email", "fritz.fantom@hotmai.com"
|
||||
|
|
Loading…
Reference in New Issue