Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversations Feature #1597

Merged
merged 1 commit into from
Dec 20, 2017
Merged

Conversations Feature #1597

merged 1 commit into from
Dec 20, 2017

Conversation

begedin
Copy link
Contributor

@begedin begedin commented Dec 11, 2017

The API changes required for this to work are in PR code-corps/code-corps-api#1301

  • rewrite to use modal for creation (see below)
  • test conversation-thread component
  • test conversation-composer component
  • test new-conversation-modal component
  • update conversation-part component tests
  • test channels
  • write (small) acceptance tests
  • allow for closing conversations
  • update socket connection to change when user becomes authenticated/unauthenticated (probably by disconnecting and reconnecting)
  • allow for reopening conversations
  • add filtering conversations by open/closed

Closes #1592
Closes #1593
Closes #1594
Closes #1596

@begedin
Copy link
Contributor Author

begedin commented Dec 15, 2017

I pushed a big commit, because I'm afraid there was a bit of a snowball effect of things that needed to work to make the PR self-contained. Will add comments to the code where applicable.

* Returns true if the user is the owner of the project.
* @type {Boolean}
*/
canAdminister: or('{userIsAdmin,userIsOwner}'),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Until this PR, all of the bigger actions on a project were accessible by owners only. The only thing admins could do was approve pending members.

import { alias } from '@ember/object/computed';

export default Controller.extend({
conversation: alias('model')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ember inspector expects a model property on the controller, so I started using this alias instead of setupController we usually do. If we don't want this, I'm fine with reverting to setupController


export default Controller.extend({
message: alias('model'),
user: alias('model.conversations.firstObject.user')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, like many other places in the code, assumes a single user per message, meaning a single conversation as well.

As we add UI support for multiple users, we will need to update this.

app/router.js Outdated
@@ -65,9 +65,13 @@ Router.map(function() {

this.route('project', { path: '/:slugged_route_slug/:project_slug' }, function() {
this.route('checkout'); // Where you enter your credit card details
this.route('contributors');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contributors was under settings, meaning, since settings is owner only, contributors was also owner only. To make it admin or owner, I had to move it.

If we move it back, each settings subroute will have to implement it's own CanMixin, while the base settings route should not have it.

If we want it here, but keep the old /:orgslug/slug/settings/contributors url, we can just set the path here.

Personally, I prefer the new placement and the new :orgslug/:slug/contributors url. I would even move donations in the same way

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this move.

// 1. Sideload project user records
// 2. Have the server compute an ability table for the current user
return get(project, 'projectUsers').then(() => {
if (this.cannot('administer project', project)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used to be cannot('manage project', project)

// peekAll returns a live array, so it will alse render any newly created
// messages, causing the list to update properly on project.messages.new
await store.query('conversation', { project_id: projectId });
return store.peekAll('conversation');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a list-detail UI here

  • a list of conversations in the sidebar on /messages/
  • clicking one of the items navigates to /messages/:id, and renders the sidebar in the outlet of the main area, while still displaying the list
  • navigating to /messages/new does the same

This model hook allows the store to detect a new conversation record and show it in the list (styled differently).

It's hacky, but it's the only way I could find to do this.

An alternative would be to clone the query result of the project.messages model into an array, then render that array in controller, instead of the query result directly.

That would allow us to push the new conversation into that array during the messages.new afterModel hook.

Pushing into the query result directly, which is what I initially tried, does not seem to be possible without resorting to using private functions and properties of DS.model

});

let conversation = store.createRecord('conversation', { user });
get(message, 'conversations').pushObject(conversation);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This causes the conversation to appear in the list of conversations in the sidebar.

// Since it's created without an ID, the store will not detect the
// association was saved and will keep it around.
// We need to store a reference to it, so we can unload it on save
let unsavedConversation = get(message, 'conversations.firstObject');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment explains it. Since we store save the two records (message and conversation) together as one, ember does not detect the conversation child has changed, so we need to unload it manually so it gets replaced with the saved record.


if (get(conversation, 'isNew')) {
conversation.destroyRecord();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will, unfortunately, mess with our proposed mixin.

// support saving child conversations alongside record
if (!json.id) {
json.included = snapshot.hasMany('conversations').map((c) => c.serialize());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how we include the conversation alongside the message in our payload when creating a message.

Apparently, ember data does not do this automatically and provides no configuration in order to do it, so I had to provide a manual implementation. It's according to spec, as far as I know, but the sad reality is, this whole process involves custom code both API and client side.

Copy link
Contributor

@snewcomer snewcomer Dec 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks about right!

Copy link
Contributor

@snewcomer snewcomer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow. This is phenomenal work!

body: attr(),
readAt: attr(),

author: belongsTo('user', { async: true }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think async: true is default...but I also like the explicitness...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't always, and I was about to ask if it is now finally the default.

},

async model() {
console.log('model');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console

Copy link
Contributor

@joshsmith joshsmith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some updates on the changes I made today:

  • removed skills from the user list
  • redesigned the users list some
  • other general design changes
  • fixed the test failures
  • notes below

app/router.js Outdated
this.route('new');
});
this.route('donate'); // Where you choose your donation amount
this.route('people');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reworked the conventions to be:

  • /contributors => /people
  • /messages => /conversations

The latter is more appropriate because /messages would actually be a different route entirely. These would be the automated/manual messages the project has sent/is actively sending to users. These would be more like /messages/auto and /messages/manual, anyway.

/converations serves more as a unified inbox for the project's conversations with users and replies to messages.

<div class="container">
<div class="conversations">
<section>
<aside>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have yet to add the styling for .conversations but it will differ from the .panel styling. The left sidebar will be a scrolling list of messages and the right side will contain two panes: left for the message thread, right for the information about the target user or project. In total, then, the UI will appear to have three panes, but the right two panes are basically the outlet for the individual conversation.

@joshsmith joshsmith changed the title Project messages page Project conversations route Dec 16, 2017
@joshsmith
Copy link
Contributor

Also note that creating a message in the UI prompts you to confirm save of the record before redirecting, even though we're redirecting after saving the record.

@joshsmith
Copy link
Contributor

joshsmith commented Dec 17, 2017

We should have a follow-up issue to change the includes serializer behavior introduced here (and in our API) whenever JSON API 1.1 is released and available in ember-data.

@joshsmith
Copy link
Contributor

joshsmith commented Dec 17, 2017

I don't want to spend a bunch of time writing acceptance tests for logic that's going to change (in terms of new message creation), so I think we should instead:

  • write a component for a new message modal on the /project/people page
  • move the logic of /conversations/new to either /project/people or the new modal, depending on what makes sense
  • remove the /conversations/new
  • write a component test
  • fix acceptance tests

let { conversations } = this.modelFor('project.conversations');
if (get(conversations, 'length') > 0) {
this.transitionTo('project.conversations.conversation', get(conversations, 'firstObject'));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a better way to handle this. Could use thoughts @begedin. The downside here is not getting the .active class on the project menu at all, which we want. We could force it some other way, but it's not ideal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by using conversations.index redirect.

@joshsmith
Copy link
Contributor

I think it's possible we're missing a message.readAt. I don't see any way to get the time the first message was read otherwise.

@joshsmith
Copy link
Contributor

In response to my above comment, I think we might be able to infer whether the conversation was read based on whether the conversation has a readAt, but I'm not positive about this.

@joshsmith
Copy link
Contributor

joshsmith commented Dec 18, 2017

A lot of changes above:

  • redirects to firstObject (the first open conversation) from the conversations.index route
  • sockets fully working
  • adds avatars to conversation parts
  • adds tooltips to those avatars
  • adds conversation-thread component that has some automagical scrolling behavior on first load / posting a new message
  • adds new message composer for the conversation

The only things half-implemented in the project messages UI are:

  • read status (can be implemented later)
  • open/closed filtering
  • opening/closing conversation

To sum up what's missing outside the scope of this specific PR, but which will need to be added before anything here is merged into develop (using this as the base branch, once things are deemed passing here):

  • channel subscriptions to add new conversations into the sidebar list
  • notification badge in the project menu for project messages
  • user-facing messages UI
  • channels subscriptions for the user
  • sending a message when joining a project
  • sending a message when accepting/denying a contributor
  • "X is typing" channel events
  • online presence for each user in the conversation (can be shown in the /projects/people UI, too)
  • notification badge in the header for user messages
  • notifications in the favicon
  • audible notifications

_onNewMessage(conversation) {
let store = get(this, 'store');
let id = get(conversation, 'id');
store.findRecord('conversation', id);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we not send the json payload for the conversation part with the event and then simply do store.pushPayload?

This would save us two queries at least.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot do that because the user who sent the message would have the payload pushed twice, and ember-data would rightly reject it.

There also may be data that has changed on the backend since the payload was sent, so best to simply update the parent model, IMO.

assert.expect(1);

set(this, 'onSend', (body) => {
assert.equal(body, 'foo', 'Correct value was send with action.');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sent

@joshsmith joshsmith force-pushed the 1592-project-messages branch from 9529832 to c24b18c Compare December 18, 2017 22:47
@joshsmith joshsmith added this to the Messages milestone Dec 19, 2017
@begedin begedin changed the title Project conversations route Conversations Feature Dec 19, 2017
@begedin begedin changed the title Conversations Feature Project Messages Feature Dec 19, 2017
@joshsmith joshsmith changed the title Project Messages Feature Conversations Feature Dec 19, 2017
@joshsmith
Copy link
Contributor

Wow, 3,333 lines of code exactly. 👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
3 participants