Merge branch 'master' into tailwind-redesign

This commit is contained in:
drusepth 2022-06-01 12:24:15 -07:00
commit 862ccbeda6
41 changed files with 2303 additions and 1650 deletions

View File

@ -78,6 +78,7 @@ gem 'thredded', git: 'https://github.com/indentlabs/thredded.git', branch: 'feat
# gem 'thredded', git: 'https://github.com/sudara/thredded', branch: 'master'
gem 'rails-ujs'
gem 'language_filter'
gem 'onebox', git: 'https://github.com/indentlabs/onebox.git', branch: 'notebook-engine'
gem 'discordrb'
# Smarts

View File

@ -5,6 +5,19 @@ GIT
specs:
birch (0.0.8)
GIT
remote: https://github.com/indentlabs/onebox.git
revision: fa5addc8d1ba6e988f54a7c4a83a770ed1d638ce
branch: notebook-engine
specs:
onebox (2.2.19)
addressable (~> 2.8.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
mustache
nokogiri (~> 1.7)
sanitize
GIT
remote: https://github.com/indentlabs/serendipitous-gem.git
revision: 393c9b664e0cbfacfc06d44dfd43898413b539a5
@ -1281,7 +1294,7 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.12.2)
concurrent-ruby (1.1.9)
concurrent-ruby (1.1.10)
connection_pool (2.2.5)
crass (1.0.6)
csv (3.2.2)
@ -1363,7 +1376,7 @@ GEM
concurrent-ruby (~> 1.0)
http (~> 4.4.0)
jwt (~> 2.2.1)
ibm_watson (2.1.3)
ibm_watson (2.2.0)
concurrent-ruby (~> 1.0)
eventmachine (~> 1.2)
faye-websocket (~> 0.11)
@ -1440,18 +1453,11 @@ GEM
netrc (0.11.0)
newrelic_rpm (8.5.0)
nio4r (2.5.8)
nokogiri (1.13.3)
nokogiri (1.13.6)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.13.3-x86_64-linux)
nokogiri (1.13.6-x86_64-linux)
racc (~> 1.4)
onebox (2.2.19)
addressable (~> 2.8.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
mustache
nokogiri (~> 1.7)
sanitize
opus-ruby (1.0.1)
ffi
orm_adapter (0.5.0)
@ -1461,7 +1467,7 @@ GEM
mime-types
mimemagic (~> 0.3.0)
terrapin (~> 0.6.0)
paranoia (2.5.2)
paranoia (2.6.0)
activerecord (>= 5.1, < 7.1)
paypal-checkout-sdk (1.0.4)
paypalhttp (~> 1.0.1)
@ -1470,12 +1476,12 @@ GEM
faraday (~> 0.15)
faraday_middleware (~> 0.12)
paypalhttp (1.0.1)
pg (1.3.4)
pg (1.3.5)
pry (0.14.1)
coderay (~> 1.1)
method_source (~> 1.0)
public_suffix (4.0.6)
puma (5.6.2)
puma (5.6.4)
nio4r (~> 2.0)
puma-heroku (2.0.0)
puma (>= 5.0, < 6.0)
@ -1605,7 +1611,7 @@ GEM
sqlite3 (1.4.2)
stackprof (0.2.19)
statsd-ruby (1.5.0)
stripe (5.45.0)
stripe (5.55.0)
stripe_event (2.3.2)
activesupport (>= 3.1)
stripe (>= 2.8, < 6)
@ -1625,7 +1631,7 @@ GEM
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unf_ext (0.0.8.1)
uniform_notifier (1.14.2)
warden (1.2.9)
rack (>= 2.0.9)
@ -1690,6 +1696,7 @@ DEPENDENCIES
meta-tags
mini_racer (~> 0.6.2)
newrelic_rpm
onebox!
paperclip
paranoia
paypal-checkout-sdk

View File

@ -19,6 +19,7 @@ Notebook.init = function() {
});
$('.slider').slider({ height: 200, indicators: false });
$('.dropdown-trigger').dropdown({ coverTrigger: false });
$('.dropdown-trigger-on-hover').dropdown({ coverTrigger: false, hover: true });
$('.tooltipped').tooltip({ enterDelay: 50 });
$('.with-character-counter').characterCounter();
$('.materialboxed').materialbox();

View File

@ -100,6 +100,7 @@ $(document).ready(function () {
});
});
$('.js-load-page-name').each(function() {
// Replace this element's content with the name of the page
var tag = $(this);
@ -130,9 +131,6 @@ $(document).ready(function () {
tag.find('.name-container').text("Unknown " + tag.data('klass'));
});
}
});
});

View File

@ -33,7 +33,6 @@ $(document).ready(function () {
$('.js-move-event-up').click(function () {
var event_container = $(this).closest('.timeline-event-container');
var event_id = event_container.data('event-id');
debugger;
$.get(
"/plan/move/timeline_events/" + event_id + "/up"
@ -117,6 +116,20 @@ $(document).ready(function () {
// Update IDs to the newly-created event
cloned_template.data('event-id', new_event_id);
cloned_template.attr('data-event-id', new_event_id);
// Update labels to jump to this event's fields
var title_field = cloned_template.find('.ref-title');
title_field.find('input').attr('id', 'timeline-event-title-' + new_event_id);
title_field.find('label').attr('for', 'timeline-event-title-' + new_event_id);
var desc_field = cloned_template.find('.ref-description');
desc_field.find('textarea').attr('id', 'timeline-event-description-' + new_event_id);
desc_field.find('label').attr('for', 'timeline-event-description-' + new_event_id);
var notes_field = cloned_template.find('.ref-notes');
notes_field.find('textarea').attr('id', 'timeline-event-notes-' + new_event_id);
notes_field.find('label').attr('for', 'timeline-event-notes-' + new_event_id);
//cloned_template.find('input[name="timeline_event[timeline_id]"]').val(timeline_id);
cloned_template.find('.js-delete-timeline-event').attr('href', '/plan/timeline_events/' + new_event_id);
cloned_template.find('.autosave-form').attr('action', '/plan/timeline_events/' + new_event_id);

View File

@ -277,4 +277,4 @@ body {
color: black;
}
}
}
}

View File

@ -2,23 +2,55 @@
#thredded--container {
#q /* search input */ {
height: 37px;
padding-left: 16px;
}
@media only screen and (min-width: 600px) {
#q /* search input */ {
height: 19px;
}
}
.thredded--navigation-breadcrumbs {
margin-top: 0.4em;
padding-left: 0.1em;
overflow: hidden;
min-height: 5em;
max-height: 60px;
li a {
padding: 0;
line-height: 2rem;
}
}
.thredded--scoped-navigation li a {
padding: 0;
line-height: 2rem;
}
.thredded--user-navigation {
height: 70px;
margin-top: 10px;
margin-right: 10px;
height: 2rem;
margin: 1rem;
border-bottom: 0;
.thredded--user-navigation--item a {
padding: 0;
padding: 8px 4px;
line-height: 2rem;
}
}
@media only screen and (max-width: 600px) {
.thredded--user-navigation {
margin: 0;
}
}
.thredded--post--user {
color: black;
a {
color: #347a36;
}
}
@ -91,9 +123,22 @@
.thredded--main-header {
nav {
background: white;
padding: 0 10px;
margin-bottom: 100px;
margin-bottom: 4rem;
line-height: 3rem;
}
@media only screen and (min-width: 600px) {
nav {
height: 3rem;
}
}
}
.thredded--topic-header {
.thredded--topic-header--title {
margin-bottom: 0.4em;
}
}
@ -122,6 +167,8 @@
}
.thredded--post--content {
padding-top: 0.6em !important;
ul {
list-style-type: inherit !important;
padding-left: 40px !important;
@ -152,3 +199,7 @@
padding-top: 5px !important;
}
}
.thredded--topic-delete--wrapper {
margin-top: 0.5rem !important;
padding-top: 1rem !important;
}

View File

@ -1,4 +1,6 @@
class ThreddedProxyController < ApplicationController
before_action :authenticate_user!, only: [:view_as_plaintext, :view_as_irc_log, :save_to_document]
def topic
topic = Thredded::Topic.find_by(slug: params[:slug])
if topic.present?
@ -10,4 +12,20 @@ class ThreddedProxyController < ApplicationController
redirect_back(fallback_location: root_path, notice: "The link you tried to visit is invalid or no longer exists.")
end
end
def view_as_plaintext
topic = Thredded::Topic.find_by(slug: params[:slug])
render plain: ForumsProsifyService.prosify_text(topic)
end
def view_as_irc_log
topic = Thredded::Topic.find_by(slug: params[:slug])
render plain: ForumsProsifyService.prosify_irc_log(topic)
end
def save_to_document
topic = Thredded::Topic.find_by(slug: params[:slug])
document = ForumsProsifyService.save_to_document(current_user, topic)
redirect_to document, notice: "Thread saved to document!"
end
end

View File

@ -30,6 +30,10 @@ class UsersController < ApplicationController
# We double up on the same returns from set_user here since we're calling set_user manually instead of a before_action
return if @user.nil?
return if @user.private_profile?
@random_image_including_private_pool_cache = ImageUpload.where(
user_id: @user.id,
).group_by { |image| [image.content_type, image.content_id] }
@content_type = content_type
@content_list = @user.send(content_type_name).is_public.order(:name)

View File

@ -107,7 +107,9 @@ class DocumentEntitiesSidebar extends React.Component {
<i className={"material-icons left " + this.classColor(entity_type) + "-text"}>
{ this.classIcon(entity_type) }
</i>
{ entity.text }
<span className='js-load-page-name' data-klass={entity_type} data-id={entity.entity_id}>
<span className='name-container'>{ entity.text }</span>
</span>
</a>
</li>
);

View File

@ -34,6 +34,8 @@ class PageLookupSidebar extends React.Component {
show_data: false,
category_open: {}
};
window.page_lookup_cache = {};
}
classIcon(class_name) {
@ -51,32 +53,45 @@ class PageLookupSidebar extends React.Component {
category_open: {}
});
// make api request
await axios.get(
"/api/v1/" + page_type.toLowerCase() + "/" + page_id
+ '?application_token=4756de490e82956dc6329e6650aaec664e27ccd27e153e2f'
+ '&authorization_token=167bb93139303904cf67f6480a29e71c9f1eaf7a28e902e1',
{
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
).then(response => {
// load response into list
if ((page_type + '-' + page_id) in window.page_lookup_cache) {
this.setState({
page_data: response.data,
show_data: !response.data.hasOwnProperty('error'),
page_data: window.page_lookup_cache[page_type + '-' + page_id],
show_data: true,
page_type: page_type,
page_id: page_id
});
}).catch(err => {
console.log(err);
this.setDrawerVisible(false);
} else {
// make new api request
await axios.get(
"/api/v1/" + page_type.toLowerCase() + "/" + page_id
+ '?application_token=4756de490e82956dc6329e6650aaec664e27ccd27e153e2f'
+ '&authorization_token=167bb93139303904cf67f6480a29e71c9f1eaf7a28e902e1',
{
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
).then(response => {
// cache response
window.page_lookup_cache[page_type + '-' + page_id] = response.data;
return null;
});
// load response into list
this.setState({
page_data: response.data,
show_data: !response.data.hasOwnProperty('error'),
page_type: page_type,
page_id: page_id
});
}).catch(err => {
console.log(err);
this.setDrawerVisible(false);
return null;
});
}
};
content_page_view_path(content_type, content_id) {

View File

@ -7,6 +7,7 @@ class NotifyDiscordOfThreadJob < ApplicationJob
thread_id = args.shift
thread = Thredded::Topic.find_by(id: thread_id)
raise "No thread found for new ID #{thread.id.inspect}" unless thread
return if thread.moderation_state == "blocked"
webhook_url = ENV.fetch('DISCORD_FORUMS_WEBHOOK', '').freeze

View File

@ -36,7 +36,10 @@ module HasImageUploads
end
def header_asset_for(class_name)
ActionController::Base.helpers.asset_path("card-headers/#{class_name.downcase.pluralize}.webp")
# Since we use this as a fallback image on SEO content (for example, Twitter cards for shared notebook pages),
# we need to include the full protocol + domain + path to ensure they will display the image. A relative path
# will not work.
"https://www.notebook.ai" + ActionController::Base.helpers.asset_url("card-headers/#{class_name.downcase.pluralize}.webp")
end
end
end

View File

@ -1,6 +1,6 @@
class DocumentEntity < ApplicationRecord
belongs_to :entity, polymorphic: true, optional: true
belongs_to :document_analysis
belongs_to :document_analysis, optional: true
after_create :match_notebook_page!, if: Proc.new { |de| de.entity_id.nil? }

View File

@ -18,7 +18,8 @@ class ImageUpload < ApplicationRecord
SecureRandom.uuid,
File.extname(filename).downcase
].join
}
},
s3_protocol: 'https'
# has_one_attached :upload
validates_attachment_content_type :src, content_type: /\Aimage\/.*\Z/

View File

@ -59,7 +59,30 @@ class ApiContentSerializer
def value_for(attribute_field, content)
case attribute_field.field_type
when 'link'
self.raw_model.send(attribute_field.old_column_source)
page_links = attribute_field.attribute_values.find_by(entity_type: content.class.name, entity_id: content.id)
if page_links.nil?
# Fall back on old relation value
# We're technically doing a double lookup here (by converting response
# to link code, then looking up again later) but since this is just stopgap
# code to standardize links in views this should be fine for now.
if attribute_field.old_column_source.present?
self.raw_model.send(attribute_field.old_column_source).map { |page| "#{page.page_type}-#{page.id}" }
else
[]
end
else
# Use new link system
begin
JSON.parse(page_links.value)
rescue
if page_links.value == ""
[]
else
"Error loading Attribute ID #{page_links.id}"
end
end
end
when 'tags'
self.page_tags

View File

@ -142,7 +142,7 @@ class ContentSerializer
end
when 'tags'
self.raw_model.page_tags
self.page_tags
else # text_area, name, universe, etc
#codesmell here: we shouldn't ever have multiple attribute values but for some reason we do sometimes (in collaboration?)

View File

@ -12,11 +12,15 @@ class ContentFormatterService < Service
# todo page slugs could be cool for this? I dunno. We probably don't want to use a
# field like name that can change ([[bob]]) or have an ambiguous link.
TOKEN_REGEX = /\[\[([^\-]+)\-([^\]]+)\]\]/
# For finding links to Notebook.ai pages in the form notebook.ai/plan/characters/12345
LINK_REGEX = /https?:\/\/(?:www\.)?(?:(?:\w)+\.)?notebook\.ai\/plan\/([\w]+)\/([\d]+)/
# Only allow linking to content type classes
# todo: we shouldn't have to map name here, but apparently rails is having a little difficulty
# https://s3.amazonaws.com/raw.paste.esk.io/Llb%2F64DJHK?versionId=19Lb_TtukDbo1J_IoCpkr.d.pwpW_vmH
VALID_LINK_CLASSES = Rails.application.config.content_types[:all].map(&:name) + %w(Timeline Document)
VALID_LINK_CLASSES = Rails.application.config.content_type_names[:all] + %w(Timeline Document)
def self.show(text:, viewing_user: User.new)
# We want to evaluate markdown first, because the markdown engine also happens
# to strip out HTML tags. So: markdown, _then_ insert content links.
@ -48,6 +52,21 @@ class ContentFormatterService < Service
end
end
# Build links for linking documents to the pages they reference
def self.links_to_replace(text)
text.scan(LINK_REGEX).map do |klass, id|
# Sanitize klass (which is plural/lower to singular/title)
sanitized_klass = klass.singularize.titleize
next unless VALID_LINK_CLASSES.include?(sanitized_klass)
{
content_type: sanitized_klass,
content_id: id,
matched_string: "https://www.notebook.ai/plan/#{klass}/#{id}"
}
end
end
def self.replacement_for_token(token, viewing_user)
return unknown_link_template(token) unless token.key?(:content_type) && token.key?(:content_id)
begin
@ -97,6 +116,28 @@ class ContentFormatterService < Service
end
end
def self.name_autoloaded_chip_template(klass_model, id)
content_tag(:span, class: 'chip') do
body = ''
if klass_model
body += content_tag(:span,
class: "js-load-page-name #{klass_model.text_color}",
data: { klass: klass_model.name, id: id }
) do
[
content_tag(:i, class: 'material-icons left', style: 'position: relative; top: 3px;') do
klass_model.icon
end,
content_tag(:span, class: 'name-container') do
"<em>Loading #{klass_model.name.downcase} ##{id}...</em>".html_safe
end
].join.html_safe
end
end
body.html_safe
end
end
def self.inline_template(class_model=nil)
content_tag(:span, class: 'inline-link') do
content_tag(:span, class: class_model ? "#{class_model.text_color}" : '') do

View File

@ -19,9 +19,6 @@ class ForumReplacementService < Service
# perspective changes, always surrounded by {} (e.g. {@reader} )
PERSPECTIVE_REPLACEMENTS = {
'@reader' => Proc.new { |trigger, user| (user.nil? ? 'anonymous reader' : user.try(:display_name)) || 'anonymous reader' }
# 'their' => Proc.new { |_, _| ["they're", "their", "there"].sample }
# 'there' => Proc.new { |_, _| ["they're", "their", "there"].sample }
# "they're" => Proc.new { |_, _| ["they're", "their", "there"].sample }
}
# turn on and off at will
@ -413,9 +410,14 @@ class ForumReplacementService < Service
def self.replace_for(text, user)
gremlins_phase = 0
replaced_text = text.dup
# Page tag replacements
replaced_text = ContentFormatterService.substitute_content_links(
replaced_text,
user
)
SPAM_WORD_REPLACEMENTS.each do |trigger, replacement|
replaced_text.gsub!(/\b#{trigger.downcase}\b/i, wrapped(replacement, trigger, 'red'))
end
@ -424,9 +426,22 @@ class ForumReplacementService < Service
replaced_text.gsub!(/{#{trigger.downcase}}/i, wrapped(replacement.call(trigger, user), trigger, 'blue'))
end
if false # not implemented: [[Character-123]] or https://www.notebook.ai/plan/characters/553 etc
REFERENCE_REPLACEMENTS.each do |trigger, replacement|
replaced_text.gsub!(/\b#{trigger.downcase}\b/i, wrapped(replacement, trigger, 'notebook-blue'))
if true # not implemented: [[Character-123]] or https://www.notebook.ai/plan/characters/553 etc
replaced_text.gsub!(/>https?:\/\/(?:www\.)?(?:(?:\w)+\.)?notebook\.ai\/plan\/([\w]+)\/([\d]+)</).each do |match|
klass = $1.singularize.titleize
linkable_whitelist = Rails.application.config.content_type_names[:all]
if linkable_whitelist.include? klass
token = {
content_type: klass,
content_id: $2.to_i,
matched_string: "Invalid #{klass}"
}
replacement = ContentFormatterService.replacement_for_token(token, User.new)
">#{replacement}<"
else
match
end
end
end

View File

@ -0,0 +1,78 @@
class ForumsProsifyService < Service
ENDLINE = "\r\n"
def self.prosify_text(thredded_topic, strip_parentheticals=true)
prose = ""
thredded_topic.posts.find_each do |post|
paragraphs = post.content.split(ENDLINE)
paragraphs.each do |paragraph|
prose += "\t#{paragraph}#{ENDLINE}" unless strip_parentheticals && post.content.start_with?('(') && post.content.end_with?(')')
end
end
prose
end
def self.prosify_irc_log(thredded_topic, strip_parentheticals=true)
prose = "-!- Topic: #{thredded_topic.title}#{ENDLINE}"
user_display_name_cache = {}
thredded_topic.posts.find_each do |post|
paragraphs = post.content.split(ENDLINE)
user_display_name_cache[post.user_id] = post.user.try(:display_name) || "Anonymous" unless user_display_name_cache.key?(post.user_id)
paragraphs.each do |paragraph|
next if paragraph.empty?
next if strip_parentheticals && post.content.start_with?('(') && post.content.end_with?(')')
prose += "<#{user_display_name_cache[post.user_id]}> #{paragraph}#{ENDLINE}"
end
end
prose
end
def self.prosify_html(thredded_topic, strip_parentheticals=true)
prose = ""
user_display_name_cache = {}
thredded_topic.posts.find_each do |post|
user_display_name_cache[post.user_id] = post.user.try(:display_name) || "Anonymous user" unless user_display_name_cache.key?(post.user_id)
tooltip = "authored by #{user_display_name_cache[post.user_id]}"
prose += "<p class='tooltipped' data-tooltip='#{tooltip}' data-position='right'>#{post.content}</p>" unless strip_parentheticals && post.content.start_with?('(') && post.content.end_with?(')')
end
prose
end
def self.save_to_document(user, thredded_topic, strip_parentheticals=true)
document = user.documents.create!(
title: "#{thredded_topic.title} (forums post)",
body: prosify_html(thredded_topic, strip_parentheticals)
)
analysis = document.document_analysis.create!
# If there are any links to Notebook.ai pages or [[page-id]] tokens in this post, we should automatically link those pages
tokens = ContentFormatterService.tokens_to_replace(document.body)
tokens.each do |token|
analysis.document_entities.find_or_create_by(
entity_type: token[:content_type],
entity_id: token[:content_id]
)
end
links = ContentFormatterService.links_to_replace(document.body)
links.each do |link|
analysis.document_entities.find_or_create_by(
entity_type: link[:content_type],
entity_id: link[:content_id],
text: link[:matched_string]
)
end
document
end
end

View File

@ -3,6 +3,7 @@
<div>
<%= image_tag asset_path("card-headers/#{@content_type_name.downcase.pluralize}.webp"), class: 'h-32 w-full object-cover lg:h-48' %>
</div>
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="-mt-12 sm:-mt-16 sm:flex sm:items-end sm:space-x-5">
<div class="flex">

View File

@ -33,13 +33,13 @@
</div>
<div class="card-action">
<% if current_user.can_update?(content) %>
<%= link_to content.edit_path, class: 'green-text right', target: content.is_a?(Document) ? '_new' : '_self' do %>
<%= link_to content.is_a?(ContentPage) ? content.edit_path : edit_polymorphic_path(content), class: 'green-text right', target: content.is_a?(Document) ? '_new' : '_self' do %>
<i class="material-icons left"><%= content_type.icon %></i>
Edit
<% end %>
<% end %>
<% if current_user.can_read?(content) %>
<%= link_to content.view_path, class: 'blue-text text-lighten-1' do %>
<%= link_to content.is_a?(ContentPage) ? content.view_path : content, class: 'blue-text text-lighten-1' do %>
<i class="material-icons left"><%= content_type.icon %></i>
View
<% end %>

View File

@ -1,8 +1,12 @@
<%
page_description = "#{@content.name}, #{@content.description} — a fictional #{@content.class.name.downcase} on Notebook.ai"
page_description ||= "#{@content.name} is a fictional #{@content.class.name.downcase} on Notebook.ai"
set_meta_tags title: @content.name,
description: "#{%w(a e i o u).include?(content.class.name.downcase[0]) ? "An" : "A"} #{@content.class.name.downcase} on Notebook.ai",
description: page_description,
image_src: @content.first_public_image,
og: { type: 'website' }
og: { type: 'website' },
twitter: { card: 'photo', image: @content.first_public_image }
%>
<%= content_for :full_width_page_header do %>

View File

@ -1,26 +1,32 @@
<%= content_for :javascript do %>
var tribute = new Tribute({
values: [
<% @linkables_cache.each do |class_name, collection| %>
<% linkable_class = content_class_from_name(class_name) %>
<% collection.each do |page_name, page_id| %>
{
key: "<%= page_name.gsub('"', "\"").gsub("\n", " ").gsub("\r", " ").strip %>",
value: '[[<%= class_name %>-<%= page_id %>]]',
color: '<%= linkable_class.color %>',
icon: '<%= linkable_class.icon %>'
},
<% if @linkables_cache %>
<%= content_for :javascript do %>
var tribute = new Tribute({
<%#
TODO: tribute allows us to populate values async, so that might be worth exploring the tradeoffs of
https://github.com/zurb/tribute
%>
values: [
<% @linkables_cache.each do |class_name, collection| %>
<% linkable_class = content_class_from_name(class_name) %>
<% collection.each do |page_name, page_id| %>
{
key: "<%= page_name.gsub('"', "\"").gsub("\n", " ").gsub("\r", " ").strip %>",
value: '[[<%= class_name %>-<%= page_id %>]]',
color: '<%= linkable_class.color %>',
icon: '<%= linkable_class.icon %>'
},
<% end %>
<% end %>
<% end %>
],
selectTemplate: function (item) {
// We're overriding the default here so we don't prepend a @
return item.original.value;
},
menuItemTemplate: function (item) {
return '<i class="material-icons right ' + item.original.color + '-text">' + item.original.icon + '</i>' + item.string;
},
spaceSelectsMatch: false
});
tribute.attach(document.querySelectorAll('.js-can-mention-pages'));
],
selectTemplate: function (item) {
// We're overriding the default here so we don't prepend a @
return item.original.value;
},
menuItemTemplate: function (item) {
return '<i class="material-icons right ' + item.original.color + '-text">' + item.original.icon + '</i>' + item.string;
},
spaceSelectsMatch: false
});
tribute.attach(document.querySelectorAll('.js-can-mention-pages'));
<% end %>
<% end %>

View File

@ -1,6 +1,6 @@
<%= view_hooks.messageboards_index.messageboard.render self, messageboard: messageboard do %>
<%= link_to messageboard.path,
class: ['thredded--messageboard',
class: ['thredded--messageboard hoverable',
*('thredded--messageboard--has-unread-topics' if messageboard.unread_topics?),
*('thredded--messageboard--has-unread-followed-topics' if messageboard.unread_followed_topics?)] do %>
<%= render 'thredded/messageboards/messageboard/header', messageboard: messageboard %>

View File

@ -1,7 +1,6 @@
<% post, content = post_and_content if local_assigns.key?(:post_and_content) %>
<%
blocked_post = user_signed_in? && post.user.present? && post.user.blocked_by?(current_user)
<%
post, content = post_and_content if local_assigns.key?(:post_and_content)
blocked_post = user_signed_in? && post.user.present? && post.user.blocked_by?(current_user)
%>
<%
@ -14,6 +13,7 @@
<div class='grey-text text-lighten-2'>
<em class="tooltipped" data-tooltip="This message is hidden because you've blocked the user it's from.">1 message hidden.</em>
</div>
<% else %>
<%= render 'thredded/posts_common/before_first_unread_post', post: post if post.first_unread_in_page? %>
<%= content_tag :article, id: dom_id(post), class: "thredded--post thredded--#{post.read_state}--post #{muted_post ? muted_post_classes : 'card'}" do %>

View File

@ -0,0 +1,7 @@
<header>
<%= image_tag post.avatar_url, class: 'thredded--post--avatar' if post.user %>
<h2 class="thredded--post--user"><%= user_link post.user %></h2>
<a href="<%= post.permalink_path %>" rel="nofollow" class="thredded--link thredded--post--created-at right">
<%= time_ago post.created_at %>
</a>
</header>

View File

@ -0,0 +1,13 @@
<li>
<%= form.label :content, content_label %>
<%= view_hooks.post_form.content_text_area.render self, form: form, content_label: content_label do %>
<%= render 'thredded/posts_common/form/before_content', form: form %>
<%= form.text_area :content, {rows: 5, required: true, autofocus: !!params[:autofocus_new_post_content], class: 'js-can-mention-pages'} %>
<%= render 'thredded/posts_common/form/after_content', form: form %>
<% end %>
<div>
<%= link_to 'https://www.markdownguide.org/basic-syntax/', target: '_blank' do %>
<span class="hoverable badge new blue white-text" data-badge-caption="formatting help"></span>
<% end %>
</div>
</li>

View File

@ -0,0 +1,99 @@
<%# @type preferences [Thredded::UserPreferencesForm] %>
<%= form_for(preferences, method: :patch, url: preferences.update_path, html: {
class: 'thredded--form thredded--notification-preferences-form',
'data-thredded-user-preferences-form' => true
}) do |f| %>
<div class="card-panel">
<ul class="thredded--form-list thredded--user-preferences-global">
<li class="thredded--user-preferences--auto-follow-topics">
<%= f.label :auto_follow_topics do %>
<%= f.check_box :auto_follow_topics,
'data-thredded-update-checkbox-on-change' =>
'user_preferences_form[messageboard_auto_follow_topics]' %>
<%= t 'thredded.preferences.form.auto_follow_topics.label' %>
<p class="thredded--form-list--hint">
<%= t 'thredded.preferences.form.auto_follow_topics.hint' %>
</p>
<% end %>
</li>
<li class="thredded--user-preferences--follow-topics-on-mention">
<%= f.label :follow_topics_on_mention do %>
<%= f.check_box :follow_topics_on_mention, 'data-thredded-bound-messageboard-pref' => 'user_preferences_form[messageboard_follow_topics_on_mention]' %>
<%= t 'thredded.preferences.form.follow_topics_on_mention.label' %>
<p class="thredded--form-list--hint">
<%= t 'thredded.preferences.form.follow_topics_on_mention.hint' %>
</p>
<% end %>
</li>
<% if Thredded.notifiers.present? %>
<li class="thredded--user-preferences--notifications-for-followed-topics">
<label><%= t 'thredded.preferences.form.notifications_for_followed_topics.label' %></label>
<%= f.fields_for :notifications_for_followed_topics, preferences.notifications_for_followed_topics do |fn| %>
<%= fn.label :enabled do %>
<%= fn.hidden_field :notifier_key %>
<%= fn.check_box :enabled,
'data-thredded-bound-messageboard-pref' =>
"user_preferences_form[messageboard_notifications_for_followed_topics_attributes][#{fn.index}][enabled]" %>
<%= fn.object.notifier_human_name %>
<%- end %>
<%- end %>
</li>
<li class="thredded--user-preferences--notifications-for-private-topics">
<label><%= t 'thredded.preferences.form.notifications_for_private_topics.label' %></label>
<%= f.fields_for :notifications_for_private_topics, preferences.notifications_for_private_topics do |fn| %>
<%= fn.label :enabled do %>
<%= fn.hidden_field :notifier_key %>
<%= fn.check_box :enabled %>
<%= fn.object.notifier_human_name %>
<% end %>
<% end %>
</li>
<% end %>
</ul>
</div>
<% if preferences.messageboard %>
<h2 class="thredded--preferences--title">
<%= t 'thredded.preferences.messageboard_preferences_title_html', messageboard: messageboard.name %>
</h2>
<div class="card-panel">
<ul class="thredded--form-list thredded--user-preferences-messageboard" data-thredded-user-preferences-form-messageboard-fields>
<li class="thredded--user-preferences--auto-follow-topics">
<%= f.label :messageboard_auto_follow_topics do %>
<%= f.check_box :messageboard_auto_follow_topics %>
<%= t 'thredded.preferences.form.messageboard_auto_follow_topics.label' %>
<p class="thredded--form-list--hint">
<%= t 'thredded.preferences.form.messageboard_auto_follow_topics.hint' %>
</p>
<% end %>
</li>
<li class="thredded--user-preferences--follow-topics-on-mention">
<%= f.label :messageboard_follow_topics_on_mention do %>
<%= f.check_box :messageboard_follow_topics_on_mention %>
<%= t 'thredded.preferences.form.messageboard_follow_topics_on_mention.label' %>
<p class="thredded--form-list--hint">
<%= t 'thredded.preferences.form.messageboard_follow_topics_on_mention.hint' %>
</p>
<% end %>
</li>
<% if Thredded.notifiers.present? %>
<li class="thredded--user-preferences--notifications-for-followed-topics">
<label><%= t 'thredded.preferences.form.messageboard_notifications_for_followed_topics.label' %></label>
<%= f.fields_for :messageboard_notifications_for_followed_topics,
preferences.messageboard_notifications_for_followed_topics do |fn| %>
<%= fn.label :enabled do %>
<%= fn.hidden_field :notifier_key %>
<%= fn.hidden_field :messageboard_id %>
<%= fn.check_box :enabled %>
<%= fn.object.notifier_human_name %>
<% end %>
<%- end %>
</li>
<% end %>
</ul>
</div>
<% end %>
<button type="submit" class="thredded--form--submit"
data-disable-with="<%= t 'thredded.preferences.form.update_btn_submitting' %>">
<%= t 'thredded.preferences.form.submit_btn' %>
</button>
<% end %>

View File

@ -1,9 +1,11 @@
<% if Thredded.show_topic_followers %>
<div class="thredded--topic-followers">
<% if topic.followers.present? %>
<%= t('thredded.topics.followed_by')%>
<%= pluralize topic.followers.count, 'user' %>
<%# t('thredded.topics.followed_by') %>
<span class="tooltipped" data-tooltip="<%= pluralize topic.followers.count, 'person' %> will be notified of any updates to this thread." data-position="right">
<i class="material-icons tiny" style="position: relative; top: 2px;">people_alt</i>
<%= pluralize topic.followers.count, 'follower' %>
</span>
<%# topic.followers.each do |user| %>
<%#= user_link(user) %>
<%# end %>

View File

@ -0,0 +1,68 @@
<header class="thredded--topic-header">
<%= view_hooks.topic_page.title.render self, topic: topic do %>
<h1 class="thredded--topic-header--title">
<i class="material-icons left blue-text">forum</i>
<%= topic.title %>
<div>
<small>
<cite class="thredded--topic-header--started-by">
<%= t 'thredded.topics.started_by_html',
time_ago: time_ago(topic.created_at),
user: user_link(topic.user) %>
</cite>
</small>
</div>
<div class="thredded--post--dropdown">
<i class="material-icons thredded--post--dropdown--toggle">tune</i>
<div class="thredded--post--dropdown--actions">
<% if topic.can_update? %>
<%= link_to t('thredded.topics.edit'), topic.edit_path,
class: 'thredded--post--dropdown--actions--item',
rel: 'nofollow' %>
<% end %>
<% if user_signed_in? && params.key?(:id) %>
<%= link_to main_app.documentize_topic_path(params[:id]), class: 'thredded--post--dropdown--actions--item' do %>
Export to document
<% end %>
<%= link_to main_app.plaintext_topic_path(params[:id]), class: 'thredded--post--dropdown--actions--item', rel: 'nofollow' do %>
View as plaintext
<% end %>
<%= link_to main_app.irc_log_topic_path(params[:id]), class: 'thredded--post--dropdown--actions--item', rel: 'nofollow' do %>
View as chat log
<% end %>
<% end %>
<% if topic.can_destroy? %>
<div class="thredded--topic-delete--wrapper">
<%= button_to t('thredded.topics.delete_topic'), topic.destroy_path, method: :delete,
form_class: 'thredded--topic-delete-form',
class: 'thredded--button thredded--post--dropdown--actions--item',
'data-confirm' => t('thredded.topics.delete_confirm') %>
</div>
<% end %>
</div>
</div>
</h1>
<% end %>
<% if thredded_current_user %>
<% if topic.followed? %>
<div class="thredded--topic-header--follow-info">
<span class="thredded--topic-header--follow-info--reason">
<%= topic_follow_reason_text topic.follow_reason %>
</span>
<%= button_to topic.unfollow_path, form: {class: 'thredded--topic-header--follow-info--unfollow'} do %>
<%= t('thredded.topics.unfollow') %>
<%= inline_svg_tag 'thredded/follow.svg', class: 'thredded--topic-header--follow-icon' %>
<% end %>
</div>
<% else %>
<div class="thredded--topic-header--follow-info">
<%= button_to t('thredded.topics.follow'), topic.follow_path,
form: {class: 'thredded--topic-header--follow-info--follow'} %>
</div>
<% end %>
<% end %>
<%= render partial: 'thredded/topics/followers', locals: {topic: topic} %>
</header>

View File

@ -0,0 +1,41 @@
<% topic = @posts.topic %>
<% content_for :thredded_page_title, topic.title %>
<% content_for :thredded_page_id, 'thredded--topic-show' %>
<% content_for :thredded_breadcrumbs, render('thredded/shared/breadcrumbs') %>
<%= thredded_page do %>
<%= content_tag :section,
id: dom_id(topic),
class: ['thredded--main-section', 'thredded--topic', *topic_css_classes(topic)] do %>
<%= render 'thredded/topics/header', topic: topic %>
<%= view_hooks.posts_common.pagination_top.render(self, posts: @posts) do %>
<footer class="thredded--pagination-top"><%= paginate @posts %></footer>
<% end %>
<%= render_posts @posts,
partial: 'thredded/posts/post',
content_partial: 'thredded/posts/content',
locals: { actions: { quote: true } } %>
<%= view_hooks.posts_common.pagination_bottom.render(self, posts: @posts) do %>
<footer class="thredded--pagination-bottom"><%= paginate @posts %></footer>
<% end %>
<% if topic.locked? %>
<p class="thredded--topic--locked-notice"><%= t 'thredded.topics.locked.message'%></p>
<% end %>
<% if policy(@new_post.post).create? %>
<div class="thredded--post-form--wrapper">
<h3 class="thredded--post-form--title"><%= t('thredded.posts.form.title_label') %></h3>
<%= render 'thredded/posts/form',
post: @new_post,
button_text: t('thredded.posts.form.create_btn'),
button_submitting_text: t('thredded.posts.form.create_btn_submitting') %>
</div>
<% end %>
<% if false && topic.can_destroy? %>
<div class="thredded--topic-delete--wrapper">
<%= button_to t('thredded.topics.delete_topic'), topic.destroy_path, method: :delete,
form_class: 'thredded--topic-delete-form',
class: 'thredded--button',
'data-confirm' => t('thredded.topics.delete_confirm') %>
</div>
<% end %>
<% end %>
<% end %>

View File

@ -1,16 +1,28 @@
<%
badge_style = 'padding: 3px 5px; font-size: 70%; font-weight: normal;'
badge_text = user.forums_badge_text
if user.favorite_page_type?
favorite_link = link_to main_app.send("#{user.favorite_page_type.downcase.pluralize}_user_path", { id: user.id }) do
content_tag(:i, class: "material-icons tiny #{content_class_from_name(user.favorite_page_type).try(:text_color)}", style: 'position: relative; top: 4px') do
"#{content_class_from_name(user.favorite_page_type).try(:icon)}"
end
end
badge_text = favorite_link + badge_text
end
%>
<% if user.respond_to?(:selected_billing_plan_id) %>
<% if user.id == 5 %>
<span style="<%= badge_style %>; background: #2196F3; color: white"><%= badge_text.presence || 'Admin' %></span>
<span style="<%= badge_style %>; background: #2196F3; color: white"><%= badge_text.presence + 'Admin' %></span>
<% elsif user.selected_billing_plan_id == 2 %>
<span style="<%= badge_style %>; background: #dedeff"><%= badge_text.presence || 'Beta Tester' %></span>
<% elsif user.selected_billing_plan_id == 3 %>
<span style="<%= badge_style %>; background: #ffdede"><%= badge_text.presence || 'Early Adopter' %></span>
<% elsif user.on_premium_plan? %>
<span style="<%= badge_style %>; background: #ffffaa"><%= badge_text.presence || 'Premium Supporter' %></span>
<% else %>
<%= favorite_link %>
<% end %>
<% end %>

View File

@ -2,9 +2,13 @@
<% if !user.thredded_anonymous? %>
<% user_path = user_path(user) %>
<% if user_path.blank? %>
<span class='thredded--username'><%= user.thredded_display_name %></span>
<span class='thredded--username'>
<%= user.thredded_display_name %>
</span>
<% else %>
<a href="<%= user_path %>"><%= user.thredded_display_name %></a>
<a href="<%= user_path %>">
<%= user.thredded_display_name %>
</a>
<%= render partial: 'thredded/users/badge', locals: { user: user } %>
<% end %>
<% else %>

View File

@ -311,16 +311,16 @@
<div class="hoverable clearfix card" style="border-top: 5px solid <%= Timeline.hex_color %>">
<div class="card-content white-text">
<span class="card-title">
<div class="input-field">
<div class="input-field ref-title">
<%= f.text_field :title, class: 'materialize-textarea js-trigger-autosave-on-change' %>
<%= f.label :title, 'Event Title' %>
<%= f.label :title, 'Event Title', for: "timeline-event-title-[[tid]]" %>
</div>
</span>
<div class="input-field">
<div class="input-field ref-description">
<%= f.text_area :description, class: 'materialize-textarea js-trigger-autosave-on-change' %>
<%= f.label :description %>
</div>
<div class="input-field">
<div class="input-field ref-notes">
<%= f.text_area :notes, class: 'materialize-textarea js-trigger-autosave-on-change' %>
<%= f.label :notes %>
</div>

View File

@ -13,8 +13,15 @@
</span>
</div>
<div class="card-content">
<%= render partial: 'content/list/list', locals: { content_list: @content_list, content_type: @content_type } %>
<%= render partial: 'content/list/cards', locals: {
content_list: @content_list,
content_type: @content_type,
how_add_another_form: false,
show_template_editor_form: false,
show_new_button: false
} %>
</div>
<div class="clearfix" style="padding-bottom: 2em"></div>
<div class="card-action">
<%= link_to "Back to #{@user.name}'s profile'", @user %>
</div>

View File

@ -174,6 +174,9 @@ end
Rails.application.config.to_prepare do
Thredded::TopicPolicy.prepend AllowUsersToDeleteOwnTopics
Thredded::PrivateTopicPolicy.prepend AllowAdminModeration
Onebox::Engine::AllowlistedGenericOnebox.allowed_domains << "notebook.ai"
Onebox::Engine::AllowlistedGenericOnebox.allowed_domains << "papercut-server.herokuapp.com"
end
Rails.application.config.to_prepare do

View File

@ -1213,11 +1213,13 @@ en:
messageboard_preferences_nav_title: Per-category Settings
messageboard:
create: Create a New Discussion Board
topics_and_posts_counts: "%{topics_count} threads / %{posts_unread} unread"
topics_and_posts_counts: "%{posts_unread} unread of %{topics_count} threads"
last_updated_by_html: "<em>%{user} commented %{time_ago}</em>"
index:
page_title: Discussions
topics:
delete_topic: Delete full discussion
edit: Edit topic
follow: Follow this discussion
form:
create_btn: Create New Discussion

View File

@ -408,8 +408,11 @@ Rails.application.routes.draw do
# get '/forum', to: 'emergency#temporarily_disabled'
# get '/forum/:wildcard', to: 'emergency#temporarily_disabled'
# get '/forum/:wildcard/:another', to: 'emergency#temporarily_disabled'
mount Thredded::Engine, at: '/forum', as: :thredded
get '/topic/:slug', to: 'thredded_proxy#topic', as: :topic
mount Thredded::Engine, at: '/forum', as: :thredded
get '/topic/:slug', to: 'thredded_proxy#topic', as: :topic
get '/plaintext/:slug', to: 'thredded_proxy#view_as_plaintext', as: :plaintext_topic
get '/irc_log/:slug', to: 'thredded_proxy#view_as_irc_log', as: :irc_log_topic
get '/save/:slug', to: 'thredded_proxy#save_to_document', as: :documentize_topic
mount StripeEvent::Engine, at: '/webhooks/stripe'

View File

@ -55,7 +55,7 @@ namespace :backfill do
desc "Backfill cached word counts on all documents"
task document_word_count_caches: :environment do
Document.where(cached_word_count: nil).where.not(body: [nil, ""]).find_each(batch_size: 500) do |document|
Document.with_deleted.where(cached_word_count: nil).where.not(body: [nil, ""]).find_each(batch_size: 500) do |document|
document.update_column(:cached_word_count, document.computed_word_count)
puts document.id
end

3187
yarn.lock

File diff suppressed because it is too large Load Diff