{"id":13411832,"url":"https://github.com/ledermann/unread","last_synced_at":"2025-04-29T14:28:17.294Z","repository":{"id":1134166,"uuid":"1011618","full_name":"ledermann/unread","owner":"ledermann","description":"Handle unread records and mark them as read with Ruby on Rails","archived":false,"fork":false,"pushed_at":"2024-12-28T08:30:47.000Z","size":360,"stargazers_count":742,"open_issues_count":21,"forks_count":121,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-04-26T14:12:47.878Z","etag":null,"topics":["activerecord","rails","ruby","rubygems","unread-records"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ledermann.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2010-10-21T09:22:09.000Z","updated_at":"2025-01-12T13:20:09.000Z","dependencies_parsed_at":"2024-01-08T17:14:45.058Z","dependency_job_id":"38375a17-511d-43f5-8cd8-7a6fc2083125","html_url":"https://github.com/ledermann/unread","commit_stats":{"total_commits":363,"total_committers":31,"mean_commits":"11.709677419354838","dds":"0.11019283746556474","last_synced_commit":"c7552bd0acd50cc7dad7b1f7f5f0b6929d2d2e86"},"previous_names":[],"tags_count":34,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ledermann%2Funread","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ledermann%2Funread/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ledermann%2Funread/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ledermann%2Funread/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ledermann","download_url":"https://codeload.github.com/ledermann/unread/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251518071,"owners_count":21602096,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["activerecord","rails","ruby","rubygems","unread-records"],"created_at":"2024-07-30T20:01:17.363Z","updated_at":"2025-04-29T14:28:17.275Z","avatar_url":"https://github.com/ledermann.png","language":"Ruby","readme":"Unread\n======\n\nRuby gem to manage read/unread status of ActiveRecord objects - and it's fast.\n\n[![Build Status](https://github.com/ledermann/unread/workflows/Test/badge.svg?branch=master)](https://github.com/ledermann/unread/actions)\n[![Maintainability](https://api.codeclimate.com/v1/badges/930c8df0f99b20324444/maintainability)](https://codeclimate.com/github/ledermann/unread/maintainability)\n[![Coverage Status](https://coveralls.io/repos/ledermann/unread/badge.svg?branch=master)](https://coveralls.io/r/ledermann/unread?branch=master)\n\n## Features\n\n* Manages unread records for anything you want readers (e.g. users) to read (like messages, documents, comments etc.)\n* Supports _mark as read_ to mark a **single** record as read\n* Supports _mark all as read_ to mark **all** records as read in a single step\n* Gives you a scope to get the unread records for a given reader\n* Needs only one additional database table\n* Most important: Great performance\n\n\n## Requirements\n\n* Ruby 3.1 or newer\n* Rails 6.1 or newer (including Rails 7.2)\n* MySQL, PostgreSQL or SQLite\n* Needs a timestamp field in your models (like created_at or updated_at) with a database index on it\n\n\n## Changelog\n\nhttps://github.com/ledermann/unread/releases\n\n\n## Installation\n\nStep 1: Add this to your Gemfile:\n\n```ruby\ngem 'unread'\n```\n\nand run\n\n```shell\nbundle\n```\n\n\nStep 2: Generate and run the migration:\n\n```shell\nrails g unread:migration\nrake db:migrate\n```\n\n## Upgrade from previous releases\n\nIf you upgrade from an older release of this gem, you should read the [upgrade notes](UPGRADE.md).\n\n\n## Usage\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  acts_as_reader\n\n  # Optional: Allow a subset of users as readers only\n  def self.reader_scope\n    where(is_admin: true)\n  end\nend\n\nclass Message \u003c ActiveRecord::Base\n  acts_as_readable on: :created_at\n\n  # The `on:` option sets the relevant attribute for comparing timestamps.\n  #\n  # The default is :updated_at, so updating a record, which was read by a\n  # reader makes it unread again.\n  #\n  # Using :created_at, only new records will show up as unread. Updating a\n  # record which was read by a reader, will NOT mark it as unread.\n  #\n  # Any other existing timestamp field can be used as `on:` option.\nend\n\nmessage1 = Message.create!\nmessage2 = Message.create!\n\n## Get unread messages for a given user\nMessage.unread_by(current_user)\n# =\u003e [ message1, message2 ]\n\nmessage1.mark_as_read! for: current_user\nMessage.unread_by(current_user)\n# =\u003e [ message2 ]\n\n## Get read messages for a given user\nMessage.read_by(current_user)\n# =\u003e [ ]\n\nmessage1.mark_as_read! for: current_user\nMessage.read_by(current_user)\n# =\u003e [ message1 ]\n\n## Get all messages including the read status for a given user\nmessages = Message.with_read_marks_for(current_user)\n# =\u003e [ message1, message2 ]\nmessages[0].unread?(current_user)\n# =\u003e false\nmessages[1].unread?(current_user)\n# =\u003e true\n\nMessage.mark_as_read! :all, for: current_user\nMessage.unread_by(current_user)\n# =\u003e [ ]\n\nMessage.read_by(current_user)\n# =\u003e [ message1, message2 ]\n\n## Get users that have not read a given message\nuser1 = User.create!\nuser2 = User.create!\n\nUser.have_not_read(message1)\n# =\u003e [ user1, user2 ]\n\nmessage1.mark_as_read! for: user1\nUser.have_not_read(message1)\n# =\u003e [ user2 ]\n\n## Get users that have read a given message\nUser.have_read(message1)\n# =\u003e [ user1 ]\n\nmessage1.mark_as_read! for: user2\nUser.have_read(message1)\n# =\u003e [ user1, user2 ]\n\nMessage.mark_as_read! :all, for: user1\nUser.have_not_read(message1)\n# =\u003e [ ]\nUser.have_not_read(message2)\n# =\u003e [ user2 ]\n\nUser.have_read(message1)\n# =\u003e [ user1, user2 ]\nUser.have_read(message2)\n# =\u003e [ user1 ]\n\n## Get all users including their read status for a given message\nusers = User.with_read_marks_for(message1)\n# =\u003e [ user1, user2 ]\nusers[0].have_read?(message1)\n# =\u003e true\nusers[1].have_read?(message2)\n# =\u003e false\n\n# Optional: Cleaning up unneeded markers\n# Do this in a cron job once a day\nMessage.cleanup_read_marks!\n```\n\n## Getting read/unread stats through a relationship\n\n```ruby\nclass Document \u003c ApplicationRecord\n  has_many :comments\nend\n\nclass Comment \u003c ApplicationRecord\n  acts_as_readable on: :created_at\n  belongs_to :document\nend\n\n# Get unread comments count for a document\ndocument = Document.find(1)\ndefault_hash = Hash.new { |h, k| h[k] = { unread: 0, total: 0 } }\ndocument.comments.with_read_marks_for(current_user).reduce(default_hash) do |hash, comment|\n  hash[comment.id][:unread] += 1 if comment.unread?(current_user)\n  hash[comment.id][:total] += 1\n  hash\nend\n# =\u003e {20=\u003e{:unread=\u003e1, :total=\u003e10}, 82=\u003e{:unread=\u003e0, :total=\u003e4}\n```\n\nUsing `with_read_marks_for` here is the key. It uses just one query and makes sure that the following `unread?` invocations use the result of the first query.\n\n## How does it work?\n\nThe main idea of this gem is to manage a list of read items for every reader **after** a certain timestamp.\n\nThe gem defines a scope doing a LEFT JOIN to this list, so your app can get the unread items in a performant manner. Of course, other scopes can be combined.\n\nIt will be ensured that the list of read items will not grow up too much:\n\n* If a user uses \"mark all as read\", his list gets deleted and the timestamp is set to the current time.\n* If a user never uses \"mark all as read\", the list will grow and grow with each item he reads. But there is help: Your app can use a cleanup method which removes unnecessary list items.\n\nOverall, this gem can be used for large data. Please have a look at the generated SQL queries, here is an example:\n\n```ruby\n# Assuming we have a user who has marked all messages as read on 2010-10-20 08:50\ncurrent_user = User.find(42)\n\n# Get the unread messages for this user\nMessage.unread_by(current_user)\n```\n\nGenerated query:\n\n```sql\nSELECT messages.*\nFROM messages\nLEFT JOIN read_marks ON read_marks.readable_type = \"Message\"\n                    AND read_marks.readable_id = messages.id\n                    AND read_marks.reader_id = 42\n                    AND read_marks.reader_type = 'User'\n                    AND read_marks.timestamp \u003e= messages.created_at\nWHERE read_marks.id IS NULL\nAND messages.created_at \u003e '2010-10-20 08:50:00'\n```\n\nHint: You should add a database index on `messages.created_at`.\n\n\nCopyright (c) 2010-2023 [Georg Ledermann](https://ledermann.dev) and [contributors](https://github.com/ledermann/unread/graphs/contributors), released under the MIT license\n","funding_links":[],"categories":["Ruby","Rails Plugins","ORM/ODM Extensions"],"sub_categories":["Rails Activity Feeds"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fledermann%2Funread","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fledermann%2Funread","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fledermann%2Funread/lists"}