diff --git a/changelog.md b/changelog.md index ffcbfce6..25cd5fec 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,9 @@ # Changelog for Weavy +## 8.7.0 (2021-11-02) + +* Added Conversations API with functionality for getting, creating and updating Weavy conversations. For more information, check out the api documentation at https://[weavy_url]/api. + ## 8.6.8 (2021-09-21) * Fix for server error when fetching spaces. diff --git a/lib/Weavy.Bundler.dll b/lib/Weavy.Bundler.dll index c65b3e0e..880c5610 100644 Binary files a/lib/Weavy.Bundler.dll and b/lib/Weavy.Bundler.dll differ diff --git a/lib/Weavy.Bundler.pdb b/lib/Weavy.Bundler.pdb index e16b5183..05ce018a 100644 Binary files a/lib/Weavy.Bundler.pdb and b/lib/Weavy.Bundler.pdb differ diff --git a/lib/Weavy.Core.dll b/lib/Weavy.Core.dll index 77c1c1f6..b95aa1b2 100644 Binary files a/lib/Weavy.Core.dll and b/lib/Weavy.Core.dll differ diff --git a/lib/Weavy.Core.pdb b/lib/Weavy.Core.pdb index ecb596f3..4d564efe 100644 Binary files a/lib/Weavy.Core.pdb and b/lib/Weavy.Core.pdb differ diff --git a/lib/Weavy.Web.dll b/lib/Weavy.Web.dll index 13e19ced..2b53ee7e 100644 Binary files a/lib/Weavy.Web.dll and b/lib/Weavy.Web.dll differ diff --git a/lib/Weavy.Web.pdb b/lib/Weavy.Web.pdb index c00ca5a8..22dc2dc9 100644 Binary files a/lib/Weavy.Web.pdb and b/lib/Weavy.Web.pdb differ diff --git a/lib/Weavy.Web.xml b/lib/Weavy.Web.xml index 52d88a49..9e2235bf 100644 --- a/lib/Weavy.Web.xml +++ b/lib/Weavy.Web.xml @@ -6040,6 +6040,79 @@ Performs custom validation. + + + Model for creating a conversation. + + + + + The members of the conversation. + + + + + View model for a conversation. + + + + + Gets the id of the conversation. + + + + + Gets or sets the title of the conversation. + + + + + Gets the avatar url. + + + + + Gets a value indicating the presence status of the other user in a one-to-one conversation. + + + + + Gets a value indicating whether this is a chat room or a conversation between just 2 people. + + + + + Gets a value indicating if the current user has read all messages in the conversation. + + + + + Gets a value indicating if the current user has pinned the conversation. + + + + + Gets a value indicating if the current user has starred the conversation. + + + + Gets a snippet of the most recent message in the conversation. + + + + Gets the date and time when the last message in the convesation was recieved or null if no messages exists in the conversation. + + + + + Gets a string representation of when the created date occured, e.g. "4:38 PM", "Yesterday", "Wednesday", "June 16", "10/06/16" etc + + + + + Gets the list of users that are members of the conversation. + + Model for creating new meetings @@ -6105,6 +6178,101 @@ + + + Model for inserting a message into a conversation. + + + + + The message text. + + + + + View model for a message. + + + + + Gets the id of the message. + + + + + Gets the date when the message was created + + + + + Gets the user's id that created the message + + + + + Gets the user's name that created the message + + + + + Gets the user's thumb that created the message + + + + + Gets the attachments for the message + + + + + Gets the html to display + + + + + A list om conversation members that have seen the message + + + + + Model for updating the name of a room. + + + + + The new room name. + + + + + Model for settings. + + + + + Gets or sets the user time zone. + + + + + Gets or sets a value indicating whether Enter should send a message or insert a new line. + + + + + The blob id of the avatar to use or null if no avatar is set. + + + + + The avatar thumb. + + + + + A list with all available time zones. + + A model representing a Zoom webhook event diff --git a/lib/wvy.exe b/lib/wvy.exe index a26c2fca..35a66d38 100644 Binary files a/lib/wvy.exe and b/lib/wvy.exe differ diff --git a/lib/wvy.pdb b/lib/wvy.pdb index 94d93fbc..4606b7e5 100644 Binary files a/lib/wvy.pdb and b/lib/wvy.pdb differ diff --git a/src/Areas/Api/Controllers/AppsController.cs b/src/Areas/Api/Controllers/AppsController.cs index f869f761..91cfe2fd 100644 --- a/src/Areas/Api/Controllers/AppsController.cs +++ b/src/Areas/Api/Controllers/AppsController.cs @@ -40,7 +40,6 @@ public class AppsController : WeavyApiController { /// The to insert. /// /// POST /api/spaces/1/apps - /// /// { /// "name": "Files", /// "guid": "523edd88-4bbf-4547-b60f-2859a6d2ddc1" @@ -68,7 +67,6 @@ public class AppsController : WeavyApiController { /// Contains the new properties for the app. /// /// PATCH /api/spaces/527/apps - /// /// { /// "name": "Files" /// } diff --git a/src/Areas/Api/Controllers/BlobsController.cs b/src/Areas/Api/Controllers/BlobsController.cs new file mode 100644 index 00000000..ce17afb6 --- /dev/null +++ b/src/Areas/Api/Controllers/BlobsController.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web.Http; +using System.Web.Http.Description; +using Weavy.Core; +using Weavy.Core.Models; +using Weavy.Core.Services; +using Weavy.Web.Api.Controllers; +using Weavy.Web.Api.Models; +using Weavy.Web.Api.Streamers; + +namespace Weavy.Areas.Api.Controllers { + + /// + /// Api controller for manipulating Blobs. + /// + [RoutePrefix("api")] + public class BlobsController : WeavyApiController { + + /// + /// Uploads new blob(s). Use multipart/form-data for the request format. + /// After upload the blobs can be used for setting avatars or as references when creating attachments and/or files. + /// + /// The uploaded blobs(s). + [HttpPost] + [Route("blobs")] + public async Task> Upload() { + // check if the request contains multipart/form-data. + if (!Request.Content.IsMimeMultipartContent()) { + throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); + } + + // write uploaded files to local disk cache + var provider = await Request.Content.ReadAsMultipartAsync(new BlobMultipartFormDataRemoteStreamProvider()); + + // iterate over the uploaded files and store them as blobs in the database + List blobs = new List(); + foreach (var data in provider.FileData) { + var blob = provider.GetBlob(data); + if (blob != null) { + try { + blob = BlobService.Insert(blob, System.IO.File.OpenRead(data.Location)); + blobs.Add(blob); + } catch { + ThrowResponseException(HttpStatusCode.InternalServerError, "Failed to upload blobs"); + } + } + } + return new ScrollableList(blobs, null, null, blobs.Count(), Request.RequestUri); + } + } +} diff --git a/src/Areas/Api/Controllers/ConversationsController.cs b/src/Areas/Api/Controllers/ConversationsController.cs new file mode 100644 index 00000000..0f8cebbd --- /dev/null +++ b/src/Areas/Api/Controllers/ConversationsController.cs @@ -0,0 +1,426 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Web.Http; +using System.Web.Http.Description; +using Weavy.Core; +using Weavy.Core.Localization; +using Weavy.Core.Models; +using Weavy.Core.Services; +using Weavy.Core.Utils; +using Weavy.Web.Api.Controllers; +using Weavy.Web.Api.Models; +using Weavy.Web.Models; + +namespace Weavy.Areas.Api.Controllers { + + /// + /// Api controller for manipulating Conversations. + /// + [RoutePrefix("api")] + public class ConversationsController : WeavyApiController { + + private static readonly StringLocalizer T = StringLocalizer.CreateInstance(); + + /// + /// Get the conversation with the specified id. + /// + /// The conversation id. + /// GET /api/conversations/527 + /// The conversation. + [HttpGet] + [ResponseType(typeof(ConversationOutModel))] + [Route("conversations/{id:int}")] + public IHttpActionResult Get(int id) { + + // read conversation + ConversationService.SetRead(id, DateTime.UtcNow); + var c = GetConversation(id); + var conversationOut = GetConversationOut(c); + + // copy members property + conversationOut.Members = c.Members; + + return Ok(conversationOut); + } + + /// + /// Get all conversations for the current user. + /// + /// GET /api/conversations + /// The users conversations. + [HttpGet] + [ResponseType(typeof(List))] + [Route("conversations")] + public IHttpActionResult List(Query query) { + var result = ConversationService.Search(new ConversationQuery(query) { OrderBy = "PinnedAt DESC, LastMessageAt DESC" }); + var conversations = new List(); + + foreach (var c in result) { + var conversationOut = GetConversationOut(c); + conversations.Add(conversationOut); + } + return Ok(conversations); + } + + /// + /// Create a new or get the existing conversation between the current and specified user. + /// + /// The to insert. + /// The conversation. + [HttpPost] + [ResponseType(typeof(Conversation))] + [Route("conversations")] + public IHttpActionResult Create(ConversationInModel model) { + string name = null; + if (model.Members.Count() > 1) { + name = string.Join(", ", model.Members.Select(u => UserService.Get(u).GetTitle())); + } + + // create new room or one-on-one conversation or get the existing one + return Ok(ConversationService.Insert(new Conversation() { Name = name }, model.Members)); + } + + /// + /// Get the messages in the specified conversation. + /// + /// The conversation id. + /// Query options for paging, sorting etc. + /// Returns a potentially paged list of the messages in the conversation. + [HttpGet] + [ResponseType(typeof(ScrollableList))] + [Route("conversations/{id:int}/messages")] + public IHttpActionResult GetMessages(int id, QueryOptions opts) { + var messagesOut = new List(); + var conversation = GetConversation(id); + var messages = ConversationService.GetMessages(id, opts); + foreach (var m in messages) { + var seenBy = GetSeenBy(m, messages, conversation.Members); + var messageOut = GetMessageOut(m, seenBy); + messagesOut.Add(messageOut); + } + messagesOut.Reverse(); + + return Ok(new ScrollableList(messagesOut, null, null, null, Request.RequestUri)); + } + + /// + /// Creates a new message in the specified conversation. + /// + /// + /// + /// + [HttpPost] + [ResponseType(typeof(Message))] + [Route("conversations/{id:int}/messages")] + public IHttpActionResult InsertMessage(int id, MessageInModel model) { + var conversation = GetConversation(id); + return Ok(MessageService.Insert(new Message { Text = model.Text, }, conversation)); + } + + /// + /// Called by current user to indicate that they are typing in a conversation. + /// + /// Id of conversation. + /// + [HttpPost] + [ResponseType(typeof(Conversation))] + [Route("conversations/{id:int}/typing")] + public IHttpActionResult StartTyping(int id) { + var conversation = GetConversation(id); + // push typing event to other conversation members + PushService.PushToUsers(PushService.EVENT_TYPING, new { Conversation = id, User = WeavyContext.Current.User, Name = WeavyContext.Current.User.Profile.Name ?? WeavyContext.Current.User.Username }, conversation.MemberIds.Where(x => x != WeavyContext.Current.User.Id)); + return Ok(conversation); + } + + /// + /// Add members to the conversation. + /// + /// Id of conversation. + /// Ids of users to add. + /// 200 OK + [HttpPost] + [ResponseType(typeof(Conversation))] + [Route("conversations/{id:int}/members")] + public IHttpActionResult AddMembers(int id, [FromBody]int[] users) { + foreach (var userId in users) { + ConversationService.AddMember(id, userId); + } + return Ok(GetConversation(id)); + } + + /// + /// Remove members from the conversation. + /// + /// Id of conversation. + /// Id of member to remove from conversation. + /// 200 OK + [HttpDelete] + [ResponseType(typeof(Conversation))] + [Route("conversations/{id:int}/members/{user:int}")] + public IHttpActionResult RemoveMember(int id, int user) { + ConversationService.RemoveMember(id, user); + return Ok(GetConversation(id)); + } + + /// + /// Set the room name. + /// + /// Id of conversation. + /// Room name, or null to remove existing name. + /// The updated conversation. + [HttpPut] + [ResponseType(typeof(Conversation))] + [Route("conversations/{id:int}/name")] + public IHttpActionResult SetName(int id, NameInModel model) { + var conversation = GetConversation(id); + if (conversation == null || !conversation.IsRoom) { + ThrowResponseException(HttpStatusCode.NotFound, "Conversation " + id + " not found."); + } + if (model.Name.IsNullOrWhiteSpace()) { + model.Name = null; + } + conversation.Name = model.Name; + conversation = ConversationService.Update(conversation); + + // HACK: set Name to GetTitle() so that returned json has conversation title in the name property + conversation.Name = conversation.GetTitle(); + return Ok(conversation); + } + + /// + /// Called by current user to indicate that they are no longer typing. + /// + /// + /// The conversation. + [HttpDelete] + [ResponseType(typeof(Conversation))] + [Route("conversations/{id:int}/typing")] + public IHttpActionResult StopTyping(int id) { + var conversation = GetConversation(id); + // push typing event to other conversation members + PushService.PushToUsers("typing-stopped.weavy", new { Conversation = id, User = WeavyContext.Current.User, Name = WeavyContext.Current.User.Profile.Name ?? WeavyContext.Current.User.Username }, conversation.MemberIds.Where(x => x != WeavyContext.Current.User.Id)); + return Ok(conversation); + } + + /// + /// Marks a conversation as read for the current user. + /// + /// Id of the conversation to mark as read. + /// The read conversation. + [HttpPost] + [ResponseType(typeof(Conversation))] + [Route("conversations/{id:int}/read")] + public IHttpActionResult Read(int id) { + ConversationService.SetRead(id, readAt: DateTime.UtcNow); + return Ok(ConversationService.Get(id)); + } + + /// + /// Mark specified conversation as unread. + /// + /// Conversation id. + /// 200 OK + [HttpPost] + [ResponseType(typeof(Conversation))] + [Route("conversations/{id:int}/unread")] + public IHttpActionResult SetUnread(int id) { + ConversationService.SetRead(id, null); + return Ok(ConversationService.Get(id)); + } + + /// + /// Get the number of unread conversations. + /// + /// The number of unread conversations. + [HttpGet] + [ResponseType(typeof(int))] + [Route("conversations/unread")] + public IHttpActionResult GetUnread() { + return Ok(ConversationService.GetUnread().Count()); + } + + /// + /// Pins the conversation. + /// + /// Conversation id. + /// 200 OK + [HttpPost] + [Route("conversations/{id:int}/pin")] + public IHttpActionResult Pin(int id) { + ConversationService.SetPinned(id, DateTime.UtcNow); + return Ok(); + } + + /// + /// Unpins the conversation. + /// + /// Conversation id. + /// 200 OK + [HttpPost] + [Route("conversations/{id:int}/unpin")] + public IHttpActionResult UnPin(int id) { + ConversationService.SetPinned(id, null); + return Ok(); + } + + /// + /// Stars the conversation. + /// + /// Conversation id. + /// 200 OK + [HttpPost] + [Route("conversations/{id:int}/star")] + public IHttpActionResult Star(int id) { + var conversation = GetConversation(id); + EntityService.Star(conversation); + return Ok(); + } + + /// + /// Unstars the conversation. + /// + /// Conversation id. + /// 200 OK + [HttpPost] + [Route("conversations/{id:int}/unstar")] + public IHttpActionResult UnStar(int id) { + var conversation = GetConversation(id); + EntityService.Unstar(conversation); + return Ok(); + } + + /// + /// Returns the user settings. + /// + /// The user settings. + [HttpGet] + [ResponseType(typeof(SettingsModel))] + [Route("conversations/settings")] + public IHttpActionResult GetSettings() { + var settings = new SettingsModel { + EnterToSend = User.Profile.Value(UserUtils.EnterToSendKey) ?? false, + AvatarId = User.Avatar?.Id, + ThumbnailUrl = User.AvatarUrl(size: 128), + TimeZones = TimeZoneInfo.GetSystemTimeZones().Select(x => new { Label = x.DisplayName, Value = x.Id }) + }; + + if (User.Profile.Value(UserUtils.TimeZoneKey) != null) { + var tz = TimeZoneInfo.GetSystemTimeZones().FirstOrDefault(x => x.Id == User.Profile.Value(UserUtils.TimeZoneKey)); + if (tz != null) { + settings.TimeZone = new { Label = tz.DisplayName, Value = tz.Id }; + } + } + return Ok(settings); + } + + /// + /// Saves the settings. + /// + /// The updated settings. + [HttpPost] + [ResponseType(typeof(SettingsModel))] + [Route("conversations/settings")] + public IHttpActionResult SaveSettings(SettingsModel settings) { + User.Profile[UserUtils.TimeZoneKey] = settings.TimeZone; + User.Profile[UserUtils.EnterToSendKey] = settings.EnterToSend; + + if (settings.AvatarId == null) { + User.Avatar = null; + } else if (settings.AvatarId.Value != User.Avatar?.Id) { + var blob = BlobService.Get(settings.AvatarId.Value); + + if (blob != null) { + User.Avatar = blob; + } + } + UserService.Update(User); + return Ok(settings); + } + + /// + /// Gets a conversation by its id. Throws a 404 http error if the conversation is not found. + /// + /// + private Conversation GetConversation(int id) { + var conversation = ConversationService.Get(id); + if (conversation == null) { + ThrowResponseException(HttpStatusCode.NotFound, "Conversation with id " + id + " not found"); + } + return conversation; + } + + /// + /// Returns a object from a . + /// + /// + /// + private ConversationOutModel GetConversationOut(Conversation conversation) { + var conversationOut = new ConversationOutModel() { + Id = conversation.Id, + AvatarUrl = conversation.AvatarUrl(48), + Title = conversation.GetTitle(), + IsPinned = conversation.IsPinned, + IsStarred = conversation.IsStarred(), + IsRead = conversation.IsRead, + IsRoom = conversation.IsRoom, + Excerpt = (conversation.LastMessage?.CreatedById == User.Id ? T["You:"] + " " : "") + conversation.GetExcerpt(true) + }; + + if (!conversation.IsRoom) { + conversationOut.Presence = conversation.Members.FirstOrDefault(x => x.Id != User.Id)?.Presence; + } + + if (conversation.LastMessage != null) { + var local = conversation.LastMessage.CreatedAt.ToLocal(); + string formatted = conversation.LastMessage.CreatedAt.When(); + conversationOut.LastMessageAt = local; + conversationOut.LastMessageAtString = formatted; + } + return conversationOut; + } + + /// + /// Returns a object from a . + /// + /// + /// + /// + private MessageOutModel GetMessageOut(Message message, IEnumerable seenBy) { + return new MessageOutModel { + Id = message.Id, + CreatedAt = message.CreatedAt, + CreatedById = message.CreatedBy().Id, + CreatedByName = message.CreatedBy().Profile.Name, + CreatedByThumb = message.CreatedBy().ThumbPlaceholderUrl(), + Attachments = message.Attachments(), + Html = message.Html.ToAbsoluteUrls(), + SeenBy = seenBy + }; + } + + /// + /// Helper for displaying seen by indicator. + /// + /// The message for which to get seen by indicator. + /// The messages in the conversation. + /// The members in the conversation. + /// + private IEnumerable GetSeenBy(Message message, IEnumerable messages, IEnumerable members) { + + // get messages created after timmestamp + var after = messages.Where(x => x.CreatedAt > message.CreatedAt); + + // get other members + var others = members.Where(x => x.Id != WeavyContext.Current.User.Id); + + // return member if message is read by member and there are no later messages read by or created by member + foreach (ConversationMember m in others) { + if (m.ReadAt >= message.CreatedAt && !after.Any(x => m.ReadAt >= x.CreatedAt || m.Id == x.CreatedById)) { + yield return m; + } + } + } + } +} diff --git a/src/Areas/Api/Controllers/SearchController.cs b/src/Areas/Api/Controllers/SearchController.cs index 2bd00dce..2c4fad54 100644 --- a/src/Areas/Api/Controllers/SearchController.cs +++ b/src/Areas/Api/Controllers/SearchController.cs @@ -1,7 +1,6 @@ using System.Net; using System.Web.Http; using System.Web.Http.Description; -using Weavy.Areas.Api.Models; using Weavy.Core.Models; using Weavy.Core.Services; using Weavy.Core.Utils; diff --git a/src/Areas/Api/Controllers/SpacesController.cs b/src/Areas/Api/Controllers/SpacesController.cs index cf8d7b8f..c2751664 100644 --- a/src/Areas/Api/Controllers/SpacesController.cs +++ b/src/Areas/Api/Controllers/SpacesController.cs @@ -2,7 +2,6 @@ using System.Net; using System.Web.Http; using System.Web.Http.Description; -using Weavy.Areas.Api.Models; using Weavy.Core.Models; using Weavy.Core.Services; using Weavy.Web.Api.Controllers; diff --git a/src/Areas/Api/Controllers/UsersController.cs b/src/Areas/Api/Controllers/UsersController.cs index fd656013..9f598485 100644 --- a/src/Areas/Api/Controllers/UsersController.cs +++ b/src/Areas/Api/Controllers/UsersController.cs @@ -3,7 +3,7 @@ using System.Net; using System.Web.Http; using System.Web.Http.Description; -using Weavy.Areas.Api.Models; +using Weavy.Core; using Weavy.Core.Models; using Weavy.Core.Services; using Weavy.Core.Utils; @@ -148,5 +148,15 @@ public class UsersController : WeavyApiController { var result = UserService.Search(query); return Ok(new ScrollableList(result, Request.RequestUri)); } + + /// + /// Retrieves the current user. + /// + /// Returns a user. + [HttpGet] + [Route("users/me")] + public User GetMe() { + return UserService.Get(WeavyContext.Current.User.Id); + } } } diff --git a/src/Areas/Api/Models/SpaceMember.cs b/src/Areas/Api/Models/SpaceMember.cs deleted file mode 100644 index 32f771b8..00000000 --- a/src/Areas/Api/Models/SpaceMember.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Weavy.Core.Models; - -namespace Weavy.Areas.Api.Models { - - /// - /// Model for a space member. - /// - public class SpaceMember { - - /// - /// The id of the user to update. - /// - public int UserId { get; set; } - - /// - /// The access level for the user. - /// - public Access Access { get; set; } - } -} diff --git a/src/Areas/Api/Models/UserIn.cs b/src/Areas/Api/Models/UserIn.cs deleted file mode 100644 index 51c1d788..00000000 --- a/src/Areas/Api/Models/UserIn.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Collections.Generic; -using Weavy.Core.Models; -using Weavy.Core.Utils; -using DA = System.ComponentModel.DataAnnotations; - -namespace Weavy.Areas.Api.Models { - - /// - /// An object used to create and update a user. - /// - public class UserIn : DA.IValidatableObject { - - /// - /// The blob id of avatar to use for the profile image. - /// - public int? AvatarId { get; set; } - - /// - /// The name of the user. - /// - [StringLength(256)] - public string Name { get; set; } - - /// - /// The email address for the user. - /// - [EmailAddress] - [StringLength(256)] - public string Email { get; set; } - - /// - /// The username. - /// - [Required] - [RegularExpression(HtmlExtensions.UsernamePattern, ErrorMessage = "Invalid username. Valid characters are [a-zA-Z0-9_].")] - [StringLength(32)] - public string Username { get; set; } - - /// - /// The password to set for the user. - /// - public string Password { get; set; } - - /// - /// A comment about the user. - /// - public string Comment { get; set; } - - /// - /// The id of the directory where the user should be added. - /// - public int? DirectoryId { get; set; } - - /// - /// A flag indicating if the user should have admin privaligies. - /// - public bool IsAdmin { get; set; } - - /// - /// A flag indicating if the user is suspended. - /// - public bool IsSuspended { get; set; } - - /// - /// - /// - public virtual IEnumerable Validate(DA.ValidationContext validationContext) { - // validate password complexity - if (Password != null) { - foreach (var res in ValidationUtils.ValidatePassword(Password)) { - yield return new DA.ValidationResult(res.ErrorMessage, new[] { nameof(Password) }); - } - } - } - } -} diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index 1314d17e..12bf5c68 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -17,4 +17,4 @@ [assembly: Guid("70cffd0f-7b12-43ec-9c57-9080937a6b04")] // Assembly version -[assembly: AssemblyVersion("8.6.8")] +[assembly: AssemblyVersion("8.7.0")] diff --git a/src/Resources/Resources.txt b/src/Resources/Resources.txt index 09163016..523b7714 100644 --- a/src/Resources/Resources.txt +++ b/src/Resources/Resources.txt @@ -826,6 +826,7 @@ You must specify user id or role id= You need to specify at least one recipient.= You should specify either user id or role id= You started editing this {0} {1}= +You:= You’ll get a notification when someone updates or comments on this {0}.= Your comment...= Your computer= diff --git a/src/Weavy.csproj b/src/Weavy.csproj index 1efd8aca..23a51b79 100644 --- a/src/Weavy.csproj +++ b/src/Weavy.csproj @@ -1,4 +1,4 @@ - + @@ -6,7 +6,8 @@ Debug AnyCPU - + + 2.0 {70CFFD0F-7B12-43EC-9C57-9080937A6B04} {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} @@ -22,7 +23,8 @@ - + + true @@ -364,17 +366,17 @@ - + ..\lib\Weavy.Bundler.dll - + ..\lib\Weavy.Core.dll - + ..\lib\Weavy.Web.dll - + ..\lib\wvy.exe @@ -4309,11 +4311,11 @@ + + - - @@ -4361,12 +4363,13 @@ True True - 0 + 58586 / https://localhost:44323/ False False - + + False @@ -4417,7 +4420,7 @@ using System.Reflection%3B - + \ No newline at end of file diff --git a/tools/Weavy.Build.dll b/tools/Weavy.Build.dll index 16b2b097..8ed128dc 100644 Binary files a/tools/Weavy.Build.dll and b/tools/Weavy.Build.dll differ