Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/assets/stylesheets/ltags/SurveyTag.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

.survey-title {
margin: 10px 0 25px;
text-align: center;
text-align: left;
font-size: 1.4em;
color: var(--body-color);
}
Expand Down
6 changes: 1 addition & 5 deletions app/controllers/subforems_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,7 @@ def index
end

def new
# For testing purposes, use the first survey if available
return unless Rails.env.test?

# In test environment, use the first survey that allows resubmission
@survey = Survey.where(allow_resubmission: true).first || Survey.first
@survey = Survey.find_by(id: ENV["SUBFOREM_SURVEY_ID"].to_i) if ENV["SUBFOREM_SURVEY_ID"].present?
end

def edit
Expand Down
12 changes: 6 additions & 6 deletions app/javascript/Search/SearchForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@ export const SearchForm = forwardRef(
name="q"
placeholder={
articleContainer?.dataset?.articleId
? 'Find related posts...'
: `${locale('core.search')}...`
? locale('core.search_find_related_posts')
: locale('core.search_placeholder')
}
autoComplete="off"
aria-label="Search term"
Expand Down Expand Up @@ -308,15 +308,15 @@ export const SearchForm = forwardRef(
<div className="crayons-header--search-typeahead-footer">
<span>
{inputValue.length > 0
? 'Submit search for advanced filtering.'
: 'Displaying Algolia Recommendations — Start typing to search'}
? locale('core.search_submit_search')
: locale('core.search_displaying_recommendations')}
</span>
<a
href="https://www.algolia.com/developers/?utm_source=devto&utm_medium=referral"
target="_blank"
rel="noopener noreferrer"
>
Powered by Algolia
{locale('core.search_powered_by')}
</a>
</div>
</ul>
Expand All @@ -334,7 +334,7 @@ export const SearchForm = forwardRef(
target="_blank"
rel="noopener noreferrer"
>
Powered by <AlgoliaIcon /> Algolia
{locale('core.search_powered_by')} <AlgoliaIcon />
</a>
) : (
''
Expand Down
2 changes: 1 addition & 1 deletion app/javascript/Search/__tests__/Search.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ describe('<Search />', () => {
const searchInput = getByRole('textbox', { name: /search/i });

expect(searchInput.value).toEqual('fish');
expect(searchInput.getAttribute('placeholder')).toEqual(`${locale('core.search')}...`);
expect(searchInput.getAttribute('placeholder')).toEqual(locale('core.search_placeholder'));
expect(searchInput.getAttribute('autocomplete')).toEqual('off');
});

Expand Down
7 changes: 4 additions & 3 deletions app/javascript/analytics/dashboard.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { callHistoricalAPI, callReferrersAPI } from './client';
import { locale } from '@utilities/locale';

const activeCharts = {};

Expand Down Expand Up @@ -36,9 +37,9 @@ function writeCards(data, timeRangeLabel) {
const commentCard = document.getElementById('comments-card');
const readerCard = document.getElementById('readers-card');

readerCard.innerHTML = cardHTML(readers, `Readers ${timeRangeLabel}`);
commentCard.innerHTML = cardHTML(comments, `Comments ${timeRangeLabel}`);
reactionCard.innerHTML = cardHTML(reactions, `Reactions ${timeRangeLabel}`);
readerCard.innerHTML = cardHTML(readers, `${locale('core.dashboard_analytics_readers')} ${timeRangeLabel}`);
commentCard.innerHTML = cardHTML(comments, `${locale('core.dashboard_analytics_comments')} ${timeRangeLabel}`);
reactionCard.innerHTML = cardHTML(reactions, `${locale('core.dashboard_analytics_reactions')} ${timeRangeLabel}`);
}

function drawChart({ id, showPoints = true, title, labels, datasets }) {
Expand Down
5 changes: 3 additions & 2 deletions app/javascript/article-form/articleForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { embedGists } from '../utilities/gist';
import { submitArticle, previewArticle } from './actions';
import { EditorActions, Form, Header, Help, Preview } from './components';
import { Button, Modal } from '@crayons';
import { locale } from '@utilities/locale';
import {
noDefaultAltTextRule,
noEmptyAltTextRule,
Expand Down Expand Up @@ -479,8 +480,8 @@ export class ArticleForm extends Component {
/>

<span aria-live="polite" className="screen-reader-only">
{previewLoading ? 'Loading preview' : null}
{previewShowing && !previewLoading ? 'Preview loaded' : null}
{previewLoading ? locale('core.article_form_loading_preview') : null}
{previewShowing && !previewLoading ? locale('core.article_form_preview_loaded') : null}
</span>

{previewShowing || previewLoading ? (
Expand Down
9 changes: 5 additions & 4 deletions app/javascript/article-form/components/EditorActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import moment from 'moment';
import PropTypes from 'prop-types';
import { Options } from './Options';
import { ButtonNew as Button } from '@crayons';
import { locale } from '@utilities/locale';

export const EditorActions = ({
onSaveDraft,
Expand Down Expand Up @@ -49,15 +50,15 @@ export const EditorActions = ({

let saveButtonText;
if (isVersion1) {
saveButtonText = 'Save changes';
saveButtonText = locale('core.article_form_save_changes');
} else if (schedule) {
saveButtonText = 'Schedule';
saveButtonText = locale('core.article_form_schedule');
} else if (wasScheduled || !published) {
// if the article was saved as scheduled, and the user clears publishedAt in the post options, the save button text is changed to "Publish"
// to make it clear that the article is going to be published right away
saveButtonText = 'Publish';
saveButtonText = locale('core.article_form_publish');
} else {
saveButtonText = 'Save changes';
saveButtonText = locale('core.article_form_save_changes');
}

return (
Expand Down
3 changes: 2 additions & 1 deletion app/javascript/article-form/components/EditorBody.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import { useLayoutEffect, useRef } from 'preact/hooks';
import { locale } from '@utilities/locale';
import { Toolbar } from './Toolbar';
import { handleImagePasted } from './pasteImageHelpers';
import {
Expand Down Expand Up @@ -70,7 +71,7 @@ export const EditorBody = ({
name="body_markdown"
id="article_body_markdown"
defaultValue={defaultValue}
placeholder="Write your post content here..."
placeholder={locale('core.editor_body_placeholder')}
className="crayons-textfield crayons-textfield--ghost crayons-article-form__body__field ff-monospace fs-l h-100"
/>
</div>
Expand Down
5 changes: 3 additions & 2 deletions app/javascript/article-form/components/TagsField.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { h } from 'preact';
import { useEffect, useState } from 'preact/hooks';
import PropTypes from 'prop-types';
import { locale } from '@utilities/locale';
import { useTagsField } from '../../hooks/useTagsField';
import { TagAutocompleteOption } from '@crayons/MultiSelectAutocomplete/TagAutocompleteOption';
import { TagAutocompleteSelection } from '@crayons/MultiSelectAutocomplete/TagAutocompleteSelection';
Expand Down Expand Up @@ -34,9 +35,9 @@ export const TagsField = ({ onInput, defaultValue, switchHelpContext }) => {
staticSuggestionsHeading={
<h2 className="c-autocomplete--multi__top-tags-heading">Top tags</h2>
}
labelText="Add up to 4 tags"
labelText={locale('core.tags_field_label')}
showLabel={false}
placeholder="Add up to 4 tags..."
placeholder={locale('core.tags_field_placeholder')}
border={false}
maxSelections={4}
SuggestionTemplate={TagAutocompleteOption}
Expand Down
3 changes: 2 additions & 1 deletion app/javascript/article-form/components/Title.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { h } from 'preact';
import { useRef, useLayoutEffect } from 'preact/hooks';
import PropTypes from 'prop-types';
import { locale } from '@utilities/locale';
// We use this hook for the title field to automatically grow the height of the textarea.
// It helps keep the entire layout the way it is without having unnecessary scrolling and white spaces.
// Keep in mind this is what happens only here - in the Preact component.
Expand Down Expand Up @@ -32,7 +33,7 @@ export const Title = ({ onChange, defaultValue, switchHelpContext }) => {
type="text"
id="article-form-title"
aria-label="Post Title"
placeholder="New post title here..."
placeholder={locale('core.editor_new_title')}
autoComplete="off"
value={defaultValue}
onFocus={switchHelpContext}
Expand Down
5 changes: 3 additions & 2 deletions app/javascript/packs/initializers/initializeCommentPreview.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* global activateRunkitTags */
import { locale } from '@utilities/locale';

function getAndShowPreview(preview, editor) {
function attachTwitterTimelineScript() {
Expand Down Expand Up @@ -46,12 +47,12 @@ function handleCommentPreview(event) {
if (editor.value !== '') {
if (form.classList.contains('preview-open')) {
form.classList.toggle('preview-open');
trigger.innerHTML = 'Preview';
trigger.innerHTML = locale('core.comments_preview');
} else {
getAndShowPreview(preview, editor);
const editorHeight = editor.offsetHeight + 43; // not ideal but prevents jumping screen
preview.style.minHeight = `${editorHeight}px`;
trigger.innerHTML = 'Continue editing';
trigger.innerHTML = locale('core.comments_continue_editing');
form.classList.toggle('preview-open');
}
}
Expand Down
18 changes: 17 additions & 1 deletion app/mailers/digest_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ def digest_email
@articles = params[:articles]
@billboards = params[:billboards]
@unsubscribe = generate_unsubscribe_token(@user.id, :email_digest_periodic)
@user_follows_any_subforems = user_follows_any_subforems?

subject = generate_title

Expand All @@ -19,12 +20,27 @@ def digest_email
mail(to: @user.email, subject: subject)
end

def user_follows_any_subforems?
user_activity = @user.user_activity
followed_subforem_ids = user_activity&.alltime_subforems || []
default_subforem_id = Subforem.cached_default_id

# Check if user follows any subforems OR has a custom onboarding subforem
followed_subforem_ids.any? ||
(@user.onboarding_subforem_id.present? && @user.onboarding_subforem_id != default_subforem_id)
end

private

def generate_title
# Winner of digest_title_03_11
if ForemInstance.dev_to?
"#{@articles.first.title} | DEV Digest"
# Check if user follows any subforems
if user_follows_any_subforems?
"#{@articles.first.title} | Forem Digest"
else
"#{@articles.first.title} | DEV Digest"
end
else
@articles.first.title
end
Expand Down
121 changes: 96 additions & 25 deletions app/services/email_digest_article_collector.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,90 @@ def articles_to_send
return [] unless should_receive_email?

articles = if @user.cached_followed_tag_names.any?
experience_level_rating = @user.setting.experience_level || 5
experience_level_rating_min = experience_level_rating - 4
experience_level_rating_max = experience_level_rating + 4
# Set subforem context for followed subforems or default
set_subforem_context

@user.followed_articles
.select(:title, :description, :path, :cached_user, :cached_tag_list)
.published.from_subforem
articles_query = @user.followed_articles
.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id)
.published
.full_posts
.where("published_at > ?", cutoff_date)
.where(email_digest_eligible: true)
.not_authored_by(@user.id)
.where("score > ?", 8)
.where("experience_level_rating > ? AND experience_level_rating < ?",
experience_level_rating_min, experience_level_rating_max)
.order(order)
.limit(RESULTS_COUNT)

# Only filter by subforem if we're not skipping subforem filtering
articles_query = articles_query.where(subforem_id: @subforem_ids) unless @skip_subforem_filtering

articles_query.order(order).limit(RESULTS_COUNT)
else
tags = @user.cached_followed_tag_names_or_recent_tags
Article.select(:title, :description, :path, :cached_user, :cached_tag_list)
.published.from_subforem
.where("published_at > ?", cutoff_date)
.where(email_digest_eligible: true)
.not_authored_by(@user.id)
.where("score > ?", 11)
.order(order)
.limit(RESULTS_COUNT)
.merge(Article.featured.or(Article.cached_tagged_with_any(tags)))
# Set subforem context for followed subforems or default
set_subforem_context

if @skip_subforem_filtering
# If skipping subforem filtering, get articles from anywhere
articles_query = Article.select(
:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id
)
.published
.full_posts
.where("published_at > ?", cutoff_date)
.where(email_digest_eligible: true)
.not_authored_by(@user.id)
.where("score > ?", 11)
.merge(Article.featured.or(Article.cached_tagged_with_any(tags)))
.order(order)
.limit(RESULTS_COUNT)
else
# Normal logic with subforem filtering and tags
articles_query = Article.select(
:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id
)
.published
.full_posts
.where("published_at > ?", cutoff_date)
.where(email_digest_eligible: true)
.not_authored_by(@user.id)
.where("score > ?", 11)
.where(subforem_id: @subforem_ids)
.order(order)
.limit(RESULTS_COUNT)
.merge(Article.featured.or(Article.cached_tagged_with_any(tags)))
end
end

# Fallback if there are not enough articles
if articles.length < 3
articles = Article.select(:title, :description, :path, :cached_user, :cached_tag_list)
.published.from_subforem
.where("published_at > ?", cutoff_date)
.where(email_digest_eligible: true)
.where("score > ?", 11)
.not_authored_by(@user.id)
if @skip_subforem_filtering
# If we're skipping subforem filtering, get articles from anywhere
articles_query = Article.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id)
.published
.full_posts
.where("published_at > ?", cutoff_date)
.where(email_digest_eligible: true)
.where("score > ?", 11)
else
# For fallback, include both followed subforems and default subforem
fallback_subforem_ids = @subforem_ids.dup
default_subforem_id = Subforem.cached_default_id
if default_subforem_id && fallback_subforem_ids.exclude?(default_subforem_id)
fallback_subforem_ids << default_subforem_id
end

articles_query = Article.select(:title, :description, :path, :cached_user, :cached_tag_list, :subforem_id)
.published
.full_posts
.where("published_at > ?", cutoff_date)
.where(email_digest_eligible: true)
.where("score > ?", 11)
.where(subforem_id: fallback_subforem_ids)
end

articles = articles_query.not_authored_by(@user.id)
.order(order)
.limit(RESULTS_COUNT)

if @user.cached_antifollowed_tag_names.any?
articles = articles.not_cached_tagged_with_any(@user.cached_antifollowed_tag_names)
end
Expand Down Expand Up @@ -83,6 +129,31 @@ def should_receive_email?

private

def set_subforem_context
# Get user's followed subforems from UserActivity
user_activity = @user.user_activity
followed_subforem_ids = user_activity&.alltime_subforems || []
default_subforem_id = Subforem.cached_default_id

# Check if user has a custom onboarding subforem (not nil and not default)
has_custom_onboarding = @user.onboarding_subforem_id.present? &&
@user.onboarding_subforem_id != default_subforem_id

if followed_subforem_ids.any?
# User follows subforems - use those
@subforem_ids = followed_subforem_ids
@skip_subforem_filtering = false
elsif has_custom_onboarding
# User has custom onboarding subforem - don't filter by subforem at all
@subforem_ids = []
@skip_subforem_filtering = true
else
# User doesn't follow any subforems and has no custom onboarding - use default subforem
@subforem_ids = default_subforem_id ? [default_subforem_id] : []
@skip_subforem_filtering = false
end
end

def recent_tracked_click?
@user.email_messages
.where(mailer: "DigestMailer#digest_email")
Expand Down
Loading
Loading