Add messaging option in listings (#2854)

* add prop for message modal method

* clean up comments from open inbox method

* add inbox type to listing model author

* add sample methods for opening message modal in listings and listing modal

* only show for users with open inbox and hide for self

* use contact_via_connect instead of inbox_type

* fix edit listing to include contact via connect attribute

* wip send message method

* adjust message styling

* change id name

* update destination

* remove unused methods
This commit is contained in:
Mario See 2019-05-31 13:48:45 -04:00 committed by Ben Halpern
parent 36ad407c1d
commit 19b46a86e2
10 changed files with 168 additions and 81 deletions

View file

@ -60,13 +60,13 @@
input {
font-size: 0.8em;
@include themeable(
color,
theme-color,
color,
theme-color,
$black
);
@include themeable(
background,
theme-container-accent-background,
background,
theme-container-accent-background,
$light-gray
);
border-radius: 100px;
@ -126,19 +126,19 @@
width: 100%;
max-width: 580px;
@include themeable(
background,
background,
theme-container-background,
white
);
box-sizing: border-box;
@include themeable(
border,
theme-border,
border,
theme-border,
1px solid $light-medium-gray
);
@include themeable(
box-shadow,
theme-container-box-shadow,
box-shadow,
theme-container-box-shadow,
2px 2px 8px darken($light-medium-gray, 5%)
);
@media screen and (min-width: 580px) {
@ -150,8 +150,8 @@
}
header {
@include themeable(
background,
theme-container-accent-background,
background,
theme-container-accent-background,
#f2f7fc
);
padding: 25px 22px;
@ -173,8 +173,8 @@
h2 {
font-size: 1.45em;
@include themeable(
color,
theme-color,
color,
theme-color,
#32325d
);
margin: 0px;
@ -203,13 +203,13 @@
border-radius: 3px;
border: 1px solid $light-medium-gray;
@include themeable(
background,
theme-container-accent-background,
background,
theme-container-accent-background,
$light-gray
);
@include themeable(
color,
theme-color,
color,
theme-color,
$black
);
&[type="checkbox"] {
@ -223,13 +223,13 @@
font-size: 17px;
border-radius: 3px;
@include themeable(
background,
theme-container-accent-background,
background,
theme-container-accent-background,
$light-gray
);
@include themeable(
color,
theme-color,
color,
theme-color,
$black
);
border: 1px solid $light-medium-gray;
@ -243,8 +243,8 @@
font-size: 0.8em;
padding: 8px 0px;
@include themeable(
color,
theme-secondary-color,
color,
theme-secondary-color,
$medium-gray
);
}
@ -263,8 +263,8 @@
margin: 35px auto;
display: block;
@include themeable(
box-shadow,
theme-container-box-shadow,
box-shadow,
theme-container-box-shadow,
2px 2px 8px darken($light-medium-gray, 5%)
);
}
@ -288,8 +288,8 @@
text-decoration: underline;
display: inline-block;
@include themeable(
color,
theme-anchor-color,
color,
theme-anchor-color,
$bold-blue
);
}
@ -303,8 +303,8 @@
p {
font-size: 0.8em;
@include themeable(
color,
theme-secondary-color,
color,
theme-secondary-color,
$medium-gray
);
}
@ -314,8 +314,8 @@
button {
background: transparent;
@include themeable(
border,
theme-border,
border,
theme-border,
1px solid $light-medium-gray
);
font-size: 17px;
@ -339,14 +339,14 @@
font-size: 17px;
border-radius: 3px;
@include themeable(
background,
theme-container-background,
background,
theme-container-background,
white
);
margin-top: 10px;
@include themeable(
color,
theme-color,
color,
theme-color,
$black
);
border: 1px solid $light-medium-gray;
@ -354,8 +354,8 @@
.listingform__tagsoptions {
@include themeable(
border,
theme-border,
border,
theme-border,
1px solid $light-medium-gray
);
border-bottom-left-radius: 3px;
@ -389,6 +389,22 @@
font-weight: bold;
}
.single-classified-listing-container {
width: 90%;
z-index: 20;
top: 110px;
position: fixed;
left: 0;
right: 0;
margin: 12px auto;
@media screen and (min-width: 950px) {
top: calc( 50px + 15vh);
left: 50%;
width: 600px;
margin-left: -300px; /* Negative half of width. */
}
}
.single-classified-listing {
width: 100%;
display: inline-block;
@ -401,16 +417,16 @@
font-size: 0.9em;
break-inside: avoid;
@include themeable(
background,
theme-container-background,
background,
theme-container-background,
white
);
h3 {
margin: 0px;
padding: 9px 15px;
@include themeable(
background,
theme-container-accent-background,
background,
theme-container-accent-background,
lighten($light-medium-gray, 8%)
);
border-bottom: 1px solid $light-medium-gray;
@ -435,13 +451,13 @@
padding: 10px 15px;
a {
@include themeable(
color,
theme-secondary-color,
color,
theme-secondary-color,
$dark-medium-gray
);
@include themeable(
background,
theme-container-accent-background,
background,
theme-container-accent-background,
$light-medium-gray
);
display: inline-block;
@ -455,15 +471,15 @@
.single-classified-listing-author-info {
font-size: 0.69em;
@include themeable(
color,
theme-secondary-color,
color,
theme-secondary-color,
$medium-gray
);
padding: 0px 15px 4px;
a {
@include themeable(
color,
theme-secondary-color,
color,
theme-secondary-color,
$medium-gray
);
display: inline-block;
@ -474,28 +490,57 @@
}
&.single-classified-listing--opened {
z-index: 20;
position: fixed;
top: 110px;
width: 90%;
display: inline-block;
position: relative;
left: 0px;
right: 0px;
padding-bottom: 12px;
@media screen and (min-width: 950px) {
top: calc( 50px + 15vh);
left: 50%;
width: 600px;
margin-left: -300px; /* Negative half of width. */
h3 { font-size: calc(20px + 0.05vw);}
.single-classified-listing-body {
font-size: calc(18px + 0.05vw);
}
}
}
}
}
}
form.listings-contact-via-connect {
border-radius: 3px;
z-index: 20;
position: relative;
padding: 10px 15px;
margin: 12px auto;
width: 100%;
padding-bottom: 12px;
box-shadow: none;
max-width: 100%;
text-align: right;
@media screen and (min-width: 950px) {
max-width: 600px;
}
textarea#new-message {
height: 100px;
margin: 5px auto 10px auto;
font-size: 15px;
@media screen and (min-width: 950px) {
font-size: calc(18px + 0.05vw);
}
}
.submit-button {
margin: none 10px;
padding: 0.5em;
height: 2em;
width: 4em;
font-size: 1em;
line-height: 1em;
text-align: center;
}
}
.classified-listings-modal-background {
z-index: 15;
position: fixed;
left: 0; top: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.8);
user-select: none;
}
}

View file

@ -90,11 +90,10 @@ class ChatChannelsController < ApplicationController
def create_chat
chat_recipient = User.find(params[:user_id])
valid_listing = ClassifiedListing.where({ user_id: params[:user_id], contact_via_connect: true }).limit(1)
authorize ChatChannel
if chat_recipient.inbox_type == "open"
if chat_recipient.inbox_type == "open" || valid_listing.length == 1
chat = ChatChannel.create_with_users([current_user, chat_recipient], "direct")
# get message param to generate message to send
# message_markdown = "Hi #{chat_recipient.username}! I am #{current_user.username}. I can message you on DEV Connect because your inbox is open."
message_markdown = params[:message]
message = Message.new(
chat_channel: chat,

View file

@ -76,6 +76,7 @@ class ClassifiedListingsController < ApplicationController
@classified_listing.title = params[:classified_listing][:title] if params[:classified_listing][:title]
@classified_listing.body_markdown = params[:classified_listing][:body_markdown] if params[:classified_listing][:body_markdown]
@classified_listing.tag_list = params[:classified_listing][:tag_list] if params[:classified_listing][:tag_list]
@classified_listing.contact_via_connect = params[:classified_listing][:contact_via_connect] if params[:classified_listing][:contact_via_connect]
@classified_listing.save
end
clear_listings_cache

View file

@ -12,8 +12,9 @@ export class Listings extends Component {
initialFetch: true,
currentUserId: null,
openedListing: null,
message: '',
slug: null,
page: 0,
page: 0,
showNextPageButt: false,
};
@ -108,12 +109,10 @@ export class Listings extends Component {
}
handleCloseModal = (e) => {
const { query, tags, category } = this.state;
this.setState({openedListing: null, page: 0})
this.setLocation(query, tags, category, null);
document.body.classList.remove('modal-open');
}
handleOpenModal = (e, listing) => {
@ -124,6 +123,34 @@ export class Listings extends Component {
document.body.classList.add('modal-open');
}
handleDraftingMessage = (e) => {
e.preventDefault();
this.setState({ message: e.target.value })
}
handleSubmitMessage = (e) => {
if (this.state.message.replace(/\s/g, '').length === 0) {
e.preventDefault();
return;
}
const formData = new FormData();
formData.append('user_id', this.state.openedListing.user_id);
formData.append('message', this.state.message)
formData.append('controller', 'chat_channels');
var destination = `/connect/@${this.state.openedListing.author.username}`;
getCsrfToken()
.then(sendFetch('chat-creation', formData))
.then(() => {
window.location.href = destination;
})
.catch((error) => {
console.log(error);
});
}
handleQuery = e => {
const { tags, category } = this.state;
this.setState({query: e.target.value, page: 0, listings: []})
@ -156,7 +183,7 @@ export class Listings extends Component {
loadNextPage = () => {
const { query, tags, category, slug, page } = this.state;
this.setState({page: page + 1});
this.listingSearch(query, tags, category, slug);
this.listingSearch(query, tags, category, slug);
}
setUser = () => {
@ -255,17 +282,29 @@ export class Listings extends Component {
const clearQueryButton = query.length > 0 ? <button type="button" className='classified-search-clear' onClick={this.clearQuery}>×</button> : '';
let modal = '';
let modalBg = '';
let messageModal = '';
if (openedListing) {
modalBg = <div className='classified-listings-modal-background' onClick={this.handleCloseModal} role='presentation' />
if (openedListing.contact_via_connect && openedListing.user_id !== currentUserId) {
messageModal = (
<form id="listings-message-form" className="listings-contact-via-connect" onSubmit={this.handleSubmitMessage}>
<textarea value={this.state.message} onChange={this.handleDraftingMessage} id="new-message" rows="4" cols="70" placeholder="Enter your message here..." />
<button type="submit" value="Submit" className="submit-button cta">SEND</button>
</form>
);
}
modal = (
<SingleListing
onAddTag={this.addTag}
onChangeCategory={this.selectCategory}
listing={openedListing}
currentUserId={currentUserId}
onOpenModal={this.handleOpenModal}
isOpen
/>
<div className="single-classified-listing-container">
<SingleListing
onAddTag={this.addTag}
onChangeCategory={this.selectCategory}
listing={openedListing}
currentUserId={currentUserId}
onOpenModal={this.handleOpenModal}
isOpen
/>
{messageModal}
</div>
)
}
if (initialFetch) {

View file

@ -7,7 +7,6 @@ export const SingleListing = ({listing, onAddTag, currentUserId, onChangeCategor
));
const editButton = currentUserId === listing.user_id ? <a href={`/listings/${listing.id}/edit`} className="classified-listing-edit-button">edit</a> : <a href={`/report-abuse?url=https://dev.to/listings/${listing.category}/${listing.slug}`}>report abuse</a>;
const definedClass = isOpen ? 'single-classified-listing single-classified-listing--opened' : 'single-classified-listing';
const listingCard = () => {

View file

@ -18,12 +18,13 @@ class ClassifiedListing < ApplicationRecord
length: { maximum: 128 }
validates :body_markdown, presence: true,
length: { maximum: 400 }
validate :contact_via_connect
validate :restrict_markdown_input
validate :validate_tags
validate :validate_category
algoliasearch per_environment: true do
attribute :title, :processed_html, :bumped_at, :tag_list, :category, :id, :user_id, :slug
attribute :title, :processed_html, :bumped_at, :tag_list, :category, :id, :user_id, :slug, :contact_via_connect
attribute :author do
{ username: author.username,
name: author.name,

View file

@ -49,12 +49,10 @@
</div>
</div>
<%= javascript_pack_tag "listingForm", defer: true %>
<% if false # Put connect button (future feature) %>
<div class="field">
<%= form.label "contact_via_connect" %>
<%= form.check_box "contact_via_connect" %>
</div>
<% end %>
<% if current_user.organization && current_user.org_admin %>
<div class="field">
<%= form.label "post_as_organization", "Post as #{current_user.organization.name} organization" %>

View file

@ -29,6 +29,10 @@
<%= f.label "tag_list", "Tags" %>
<%= f.text_field "tag_list", value: @classified_listing.cached_tag_list, placeholder: "8 tags max, comma separated, no spaces or special characters" %>
</div>
<div class="field">
<%= f.label "contact_via_connect" %>
<%= f.check_box "contact_via_connect" %>
</div>
<%= f.submit "Update Listings Info" %>
<% end %>
<% end %>
@ -39,4 +43,4 @@
<input type="hidden" name="classified_listing[action]" value="unpublish" />
<%= f.submit "Unpublish Listing", class: "classified-listings-unpublish" %>
<% end %>
</div>
</div>

View file

@ -22,7 +22,7 @@
<div class="home">
<div class="classifieds-container" id="classifieds-index-container"
data-category="<%= params[:category] %>" data-listings="<%= @classified_listings.to_json(
only: %i[title processed_html tag_list category id user_id slug],
only: %i[title processed_html tag_list category id user_id slug contact_via_connect],
include: {
author: { only: %i[username name], methods: %i[username profile_image_90] },
},
@ -30,7 +30,7 @@
data-allcategories="<%= ClassifiedListing.categories_for_display.to_json %>"
<% if @displayed_classified_listing %>
data-displayedlisting="<%= @displayed_classified_listing.to_json(
only: %i[title processed_html tag_list category id user_id slug],
only: %i[title processed_html tag_list category id user_id slug contact_via_connect],
include: {
author: { only: %i[username name], methods: %i[username profile_image_90] },
},

View file

@ -252,6 +252,7 @@ listings_categories.each_with_index do |category, index|
title: Faker::Lorem.sentence,
body_markdown: Faker::Markdown.random,
category: category,
contact_via_connect: true,
)
end
end